@lanes-sh/link 0.4.1 → 0.5.1
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 -9
- package/instructions/agents/lanes-link-scout.md +14 -3
- package/instructions/skills/lanes-link/SKILL.md +80 -3
- package/package.json +2 -2
- package/src/cli/argv.ts +7 -0
- package/src/cli/commands/connect/index.ts +9 -6
- package/src/cli/commands/connection.ts +298 -0
- package/src/cli/commands/operate/inspect.ts +53 -22
- package/src/cli/commands/operate/migrate.ts +100 -0
- package/src/cli/commands/operate/serve.ts +21 -0
- package/src/cli/commands/owner/assets.ts +132 -0
- package/src/cli/commands/owner/shared.ts +28 -4
- package/src/cli/commands/owner/tasks.ts +194 -0
- package/src/cli/commands/owner.ts +9 -4
- package/src/cli/config-edit.ts +33 -7
- package/src/cli/config-migrate.ts +251 -0
- package/src/cli/config-repair.ts +115 -11
- package/src/cli/dispatch-owner.ts +49 -8
- package/src/cli/lanes.ts +1 -1
- package/src/cli/main.ts +22 -3
- package/src/cli/provider-marks.ts +1 -1
- package/src/cli/runtime/registry.ts +10 -2
- package/src/cli/selection.ts +18 -0
- package/src/cli/usage.ts +20 -2
- package/src/connectivity/mail/attachments.ts +5 -1
- package/src/connectivity/mail/index.ts +6 -1
- package/src/connectivity/manifest/provider.ts +15 -2
- package/src/deployments/deploy.ts +3 -2
- package/src/deployments/prepare.ts +1 -1
- package/src/deployments/servable.ts +1 -1
- package/src/deployments/upload.ts +0 -53
- package/src/profile/index.ts +4 -0
- package/src/profile/load.ts +135 -1
- package/src/providers/assets/provider.ts +337 -0
- package/src/providers/assets/store.ts +167 -0
- package/src/providers/google/index.ts +1 -1
- package/src/providers/google/tasks/index.ts +3 -3
- package/src/providers/google/tasks/redact.ts +21 -11
- package/src/providers/index.ts +3 -3
- package/src/providers/owner.ts +39 -19
- package/src/providers/setup/plan.ts +17 -1
- package/src/providers/tasks/provider.ts +370 -0
- package/src/providers/tasks/store.ts +248 -0
- package/src/server/mcp/build.ts +1 -1
- package/src/server/mcp/instructions.ts +67 -8
package/src/cli/config-edit.ts
CHANGED
|
@@ -264,13 +264,27 @@ oauth_apps: {}
|
|
|
264
264
|
# reports — an address, a workspace — so this list says whose data is reachable
|
|
265
265
|
# without having to look anything up.
|
|
266
266
|
#
|
|
267
|
-
#
|
|
268
|
-
#
|
|
269
|
-
#
|
|
270
|
-
#
|
|
271
|
-
#
|
|
272
|
-
#
|
|
267
|
+
# The six below hold no account, and that is why they are here already: they
|
|
268
|
+
# reach your own material rather than anybody's API, so there was never anything
|
|
269
|
+
# for a connect step to authorise (ADR-050). What each one is:
|
|
270
|
+
#
|
|
271
|
+
# memory what you want remembered between sessions
|
|
272
|
+
# tasks what you have to do, each with a status
|
|
273
|
+
# assets files you want kept, by name
|
|
274
|
+
# skills procedures you have written, handed to an agent as instructions
|
|
275
|
+
# vault passwords and API keys, released one at a time
|
|
276
|
+
# setup what is connected here, and what connecting more would take
|
|
277
|
+
#
|
|
278
|
+
# Nothing is stored in any of them until you or an agent puts something there,
|
|
279
|
+
# and none of them can read an account. To switch one off, deny it below —
|
|
280
|
+
# deleting the entry no longer works, because the next connect or deploy puts it
|
|
281
|
+
# back.
|
|
273
282
|
connections:
|
|
283
|
+
- { id: main, provider: memory, account: Memory }
|
|
284
|
+
- { id: main, provider: tasks, account: Tasks }
|
|
285
|
+
- { id: main, provider: assets, account: Assets }
|
|
286
|
+
- { id: main, provider: skills, account: Skills }
|
|
287
|
+
- { id: main, provider: vault, account: Vault }
|
|
274
288
|
- { id: main, provider: setup, account: Setup }
|
|
275
289
|
|
|
276
290
|
# Only what is listed here is reachable, and an empty policy grants nothing.
|
|
@@ -284,8 +298,20 @@ connections:
|
|
|
284
298
|
# allow: ['*'] everything, which is what connect writes
|
|
285
299
|
# allow: [notion.*, gmail.*] two providers
|
|
286
300
|
# deny: [gmail.send_message] a deny always beats an allow
|
|
301
|
+
#
|
|
302
|
+
# The rules below grant each of the six its whole namespace, writes included —
|
|
303
|
+
# the same thing "connect memory" wrote when it was a command you had to run.
|
|
304
|
+
# Narrowing is one line, and these are the three worth knowing:
|
|
305
|
+
#
|
|
306
|
+
# deny: [memory.write] memory becomes read-only
|
|
307
|
+
# deny: [skills.manage.*] skills can be invoked but not authored
|
|
308
|
+
# deny: [vault.put, vault.remove] nothing new can be stored
|
|
309
|
+
#
|
|
310
|
+
# A vault read is not granted by "vault.*" alone: each stored item is its own
|
|
311
|
+
# "vault.get.<id>" capability and only appears after a restart, so a write can
|
|
312
|
+
# never hand itself a read (ADR-012).
|
|
287
313
|
policy:
|
|
288
|
-
allow: [setup.*]
|
|
314
|
+
allow: [memory.*, tasks.*, assets.*, skills.*, vault.*, setup.*]
|
|
289
315
|
deny: []
|
|
290
316
|
`;
|
|
291
317
|
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { ConfigError, renamedProviderFor, validateConfigShape, type Config } from '#profile';
|
|
2
|
+
import type { SecretStore } from '#secrets';
|
|
3
|
+
import { ConfigDocument } from './config-edit.ts';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Applying a provider rename to a profile that still names the old id.
|
|
7
|
+
*
|
|
8
|
+
* Apart from `config-repair.ts` because the subject differs, not because either
|
|
9
|
+
* file grew: that one gives a profile a surface it never had, reading a config
|
|
10
|
+
* that loads. This one runs when the config does *not* load, which is the whole
|
|
11
|
+
* difficulty. `renamedProviderFor` refuses a stale row at load (`#profile`), and
|
|
12
|
+
* every command opens the config — so one row left saying `provider: tasks`
|
|
13
|
+
* takes `status`, `start`, `plan` and `doctor` down together, for a state an
|
|
14
|
+
* upgrade put the operator in without asking. The only way back was to
|
|
15
|
+
* hand-edit YAML, which is not a thing a CLI should require to undo its own
|
|
16
|
+
* release.
|
|
17
|
+
*
|
|
18
|
+
* So this reads raw YAML through `ConfigDocument` and edits it comment-first,
|
|
19
|
+
* exactly as the other repairs do.
|
|
20
|
+
*
|
|
21
|
+
* **It never guesses.** A `tasks` row labelled anything but `Tasks` is either a
|
|
22
|
+
* pre-rename Google Tasks connection or a hand-edited built-in one, and the
|
|
23
|
+
* refusal names both because the two fixes are opposite. What decides here is
|
|
24
|
+
* evidence rather than a heuristic: a stored credential at `tasks/<id>` can only
|
|
25
|
+
* belong to the OAuth connection, because the built-in holds none and never
|
|
26
|
+
* has. With no credential this reports both readings and changes nothing.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
/** One row that has to move. */
|
|
30
|
+
export interface PendingRename {
|
|
31
|
+
readonly index: number;
|
|
32
|
+
readonly from: string;
|
|
33
|
+
readonly to: string;
|
|
34
|
+
readonly id: string;
|
|
35
|
+
readonly account: string;
|
|
36
|
+
/** `provider.id`, as the rest of the CLI addresses a connection. */
|
|
37
|
+
readonly key: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface RenameMigration {
|
|
41
|
+
/** Every stale row found, whether or not it could be moved. */
|
|
42
|
+
readonly rows: readonly PendingRename[];
|
|
43
|
+
/** What was done, or would be — spelled for display. */
|
|
44
|
+
readonly changes: readonly string[];
|
|
45
|
+
/** What was left alone, each with why. */
|
|
46
|
+
readonly blocked: readonly string[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The rows a document still spells the old way.
|
|
51
|
+
*
|
|
52
|
+
* Raw YAML, so nothing here has been through a schema — this runs on a file the
|
|
53
|
+
* loader has already refused, and every field is whatever was typed. A row
|
|
54
|
+
* missing `provider` or `account` is not a rename, it is a shape error, and
|
|
55
|
+
* reporting that is `validateConfig`'s job rather than this one's.
|
|
56
|
+
*/
|
|
57
|
+
export function pendingRenames(document: ConfigDocument): PendingRename[] {
|
|
58
|
+
const pending: PendingRename[] = [];
|
|
59
|
+
|
|
60
|
+
connectionsOf(document).forEach((row, index) => {
|
|
61
|
+
const { provider, id, account } = (row ?? {}) as {
|
|
62
|
+
provider?: unknown;
|
|
63
|
+
id?: unknown;
|
|
64
|
+
account?: unknown;
|
|
65
|
+
};
|
|
66
|
+
if (typeof provider !== 'string' || typeof id !== 'string' || typeof account !== 'string') {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const moved = renamedProviderFor({ provider, account });
|
|
71
|
+
if (moved) {
|
|
72
|
+
pending.push({ index, from: provider, to: moved.to, id, account, key: `${provider}.${id}` });
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return pending;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Report what a profile is owed, and optionally apply it.
|
|
81
|
+
*
|
|
82
|
+
* Takes the credential store rather than opening one, so the caller decides
|
|
83
|
+
* whether the target is reachable and a test needs no adapter. `apply: false` is
|
|
84
|
+
* what `doctor` prints without `--fix`: the same reading against the same
|
|
85
|
+
* evidence, with nothing written.
|
|
86
|
+
*
|
|
87
|
+
* The credential is copied *before* the config is saved, and the old reference
|
|
88
|
+
* deleted only after. A crash between the two leaves a second copy of a secret
|
|
89
|
+
* the operator already holds — recoverable, and invisible. The other order
|
|
90
|
+
* leaves a config naming a credential that is gone, which presents as a
|
|
91
|
+
* connection that lost its authorisation for no reason anyone can see.
|
|
92
|
+
*/
|
|
93
|
+
export async function migrateRenamedProviders(
|
|
94
|
+
document: ConfigDocument,
|
|
95
|
+
credentials: SecretStore,
|
|
96
|
+
options: { apply: boolean },
|
|
97
|
+
): Promise<RenameMigration> {
|
|
98
|
+
const rows = pendingRenames(document);
|
|
99
|
+
if (rows.length === 0) return { rows, changes: [], blocked: [] };
|
|
100
|
+
|
|
101
|
+
const changes: string[] = [];
|
|
102
|
+
const blocked: string[] = [];
|
|
103
|
+
const accepted: PendingRename[] = [];
|
|
104
|
+
|
|
105
|
+
for (const row of rows) {
|
|
106
|
+
if (await credentials.has(`${row.from}/${row.id}`)) {
|
|
107
|
+
accepted.push(row);
|
|
108
|
+
changes.push(`connections[${row.index}]: ${row.from} → ${row.to} (${row.key})`);
|
|
109
|
+
changes.push(`credential: ${row.from}/${row.id} → ${row.to}/${row.id}`);
|
|
110
|
+
} else {
|
|
111
|
+
blocked.push(
|
|
112
|
+
`${row.key} has no stored credential, so nothing proves it was ${row.to} rather than a ` +
|
|
113
|
+
`built-in row labelled "${row.account}" by hand — set its provider or its account in ` +
|
|
114
|
+
`the file itself`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// The policy rules second, because whether one can move depends on what is
|
|
120
|
+
// left declaring the old id once the rows above have.
|
|
121
|
+
for (const [from, to] of new Map(accepted.map((row) => [row.from, row.to]))) {
|
|
122
|
+
const policy = renamePolicyRules(document, { from, to }, {
|
|
123
|
+
stillDeclared: keepsDeclaring(document, from, accepted),
|
|
124
|
+
apply: options.apply,
|
|
125
|
+
});
|
|
126
|
+
changes.push(...policy.changes);
|
|
127
|
+
blocked.push(...policy.blocked);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (!options.apply || accepted.length === 0) return { rows, changes, blocked };
|
|
131
|
+
|
|
132
|
+
for (const row of accepted) {
|
|
133
|
+
document.setIn(['connections', row.index, 'provider'], row.to);
|
|
134
|
+
|
|
135
|
+
const value = await credentials.get(`${row.from}/${row.id}`);
|
|
136
|
+
if (value !== null) await credentials.set(`${row.to}/${row.id}`, value);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Throws unless the result is a config that loads, which is the assertion
|
|
140
|
+
// worth having here — the whole premise was that it did not.
|
|
141
|
+
await document.save();
|
|
142
|
+
|
|
143
|
+
for (const row of accepted) await credentials.delete(`${row.from}/${row.id}`);
|
|
144
|
+
|
|
145
|
+
return { rows, changes, blocked };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Rewrite the policy rules that named the old id, where that is unambiguous.
|
|
150
|
+
*
|
|
151
|
+
* A rule names a provider and never an account, so `tasks.*` written for Google
|
|
152
|
+
* Tasks has to follow the rename or the migrated connection is granted nothing
|
|
153
|
+
* — `allowedConnections` drops a provider no rule covers before policy is even
|
|
154
|
+
* consulted, so the repair would land a row that serves exactly as little as
|
|
155
|
+
* the broken one did.
|
|
156
|
+
*
|
|
157
|
+
* But a profile declaring *both* — a Google Tasks row and the built-in — has one
|
|
158
|
+
* rule serving two providers, and moving it would silently revoke the one that
|
|
159
|
+
* kept its name. That profile keeps its rule and is told to add the second,
|
|
160
|
+
* which is a sentence rather than a guess at which was meant.
|
|
161
|
+
*
|
|
162
|
+
* Both lists. A `deny` written to switch Google Tasks off means it as firmly as
|
|
163
|
+
* an allow means it on, and leaving it behind would re-enable something the
|
|
164
|
+
* operator turned off.
|
|
165
|
+
*/
|
|
166
|
+
function renamePolicyRules(
|
|
167
|
+
document: ConfigDocument,
|
|
168
|
+
provider: { from: string; to: string },
|
|
169
|
+
options: { stillDeclared: boolean; apply: boolean },
|
|
170
|
+
): { changes: string[]; blocked: string[] } {
|
|
171
|
+
const changes: string[] = [];
|
|
172
|
+
const blocked: string[] = [];
|
|
173
|
+
|
|
174
|
+
for (const field of ['allow', 'deny'] as const) {
|
|
175
|
+
const rules = document.getIn(['policy', field]) as { items?: unknown[] } | null;
|
|
176
|
+
|
|
177
|
+
(rules?.items ?? []).forEach((_item, index) => {
|
|
178
|
+
// Either spelling: a bare pattern, or `{ capability, expires_at }`. The
|
|
179
|
+
// path to it differs; the decision does not.
|
|
180
|
+
const bare = document.getIn(['policy', field, index]);
|
|
181
|
+
const path =
|
|
182
|
+
typeof bare === 'string'
|
|
183
|
+
? (['policy', field, index] as const)
|
|
184
|
+
: (['policy', field, index, 'capability'] as const);
|
|
185
|
+
|
|
186
|
+
const capability = typeof bare === 'string' ? bare : document.getIn(path);
|
|
187
|
+
if (typeof capability !== 'string') return;
|
|
188
|
+
|
|
189
|
+
const [named, ...rest] = capability.split('.');
|
|
190
|
+
if (named !== provider.from) return;
|
|
191
|
+
|
|
192
|
+
const moved = [provider.to, ...rest].join('.');
|
|
193
|
+
|
|
194
|
+
if (options.stillDeclared) {
|
|
195
|
+
blocked.push(
|
|
196
|
+
`policy.${field} keeps "${capability}" — this profile still declares a ` +
|
|
197
|
+
`"${provider.from}" connection, so add "${moved}" rather than moving it`,
|
|
198
|
+
);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (options.apply) document.setIn(path, moved);
|
|
203
|
+
changes.push(`policy.${field}: ${capability} → ${moved}`);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return { changes, blocked };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Whether a row naming the old provider survives the migration.
|
|
212
|
+
*
|
|
213
|
+
* Computed against the accepted set rather than by re-reading the document,
|
|
214
|
+
* because the same answer has to hold on a report-only run, where nothing has
|
|
215
|
+
* been rewritten yet.
|
|
216
|
+
*/
|
|
217
|
+
function keepsDeclaring(
|
|
218
|
+
document: ConfigDocument,
|
|
219
|
+
provider: string,
|
|
220
|
+
accepted: readonly PendingRename[],
|
|
221
|
+
): boolean {
|
|
222
|
+
return connectionsOf(document).some(
|
|
223
|
+
(row, index) =>
|
|
224
|
+
(row as { provider?: unknown } | null)?.provider === provider &&
|
|
225
|
+
!accepted.some((moved) => moved.index === index),
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function connectionsOf(document: ConfigDocument): unknown[] {
|
|
230
|
+
const config = document.toJSON() as { connections?: unknown } | null;
|
|
231
|
+
return Array.isArray(config?.connections) ? config.connections : [];
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* The config of a document the loader has refused, shape-checked only.
|
|
236
|
+
*
|
|
237
|
+
* `openSecretStoreFor` needs a `Config` to name the target's adapter, and the
|
|
238
|
+
* document in hand fails the check that runs *after* the schema — so this is
|
|
239
|
+
* that parse without it. A document failing the schema itself is beyond this
|
|
240
|
+
* repair, and says so rather than reporting a rename it cannot see.
|
|
241
|
+
*/
|
|
242
|
+
export function shapeOf(document: ConfigDocument): Config {
|
|
243
|
+
try {
|
|
244
|
+
return validateConfigShape(document.toJSON(), document.path);
|
|
245
|
+
} catch (error) {
|
|
246
|
+
throw new ConfigError(
|
|
247
|
+
`${document.path} is malformed beyond a provider rename, so there is nothing to migrate.\n` +
|
|
248
|
+
` ${error instanceof Error ? error.message : String(error)}`,
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
}
|
package/src/cli/config-repair.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { listProfiles } from '#profile';
|
|
2
|
+
import { ConfigDocument } from './config-edit.ts';
|
|
3
|
+
import { ok, print, style, warn } from './output.ts';
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* Giving a profile a reserved provider it is missing, without undoing a choice.
|
|
@@ -9,22 +11,49 @@ import type { ConfigDocument } from './config-edit.ts';
|
|
|
9
11
|
* was already there: that file knows how to *edit* YAML safely, and this one
|
|
10
12
|
* knows what a reserved provider needs to be reachable at all.
|
|
11
13
|
*
|
|
12
|
-
*
|
|
13
|
-
* connection row nobody would think to write
|
|
14
|
-
* connected, and `identity`, which says who the owner is. Both are repaired the
|
|
14
|
+
* The owner layer holds no account and is therefore invisible without a
|
|
15
|
+
* connection row nobody would think to write. Every one of them is repaired the
|
|
15
16
|
* same way and the rules below are subtle enough that a second copy would drift
|
|
16
17
|
* — which is the whole reason this is one function taking a provider id rather
|
|
17
|
-
* than
|
|
18
|
+
* than several that look alike.
|
|
18
19
|
*/
|
|
19
20
|
|
|
20
21
|
/** The reserved provider ids that hold no account, and the label each row carries. */
|
|
21
22
|
const RESERVED_SURFACES = {
|
|
23
|
+
memory: 'Memory',
|
|
24
|
+
tasks: 'Tasks',
|
|
25
|
+
assets: 'Assets',
|
|
26
|
+
skills: 'Skills',
|
|
27
|
+
vault: 'Vault',
|
|
22
28
|
setup: 'Setup',
|
|
23
29
|
identity: 'Identity',
|
|
24
30
|
} as const;
|
|
25
31
|
|
|
26
32
|
type ReservedSurface = keyof typeof RESERVED_SURFACES;
|
|
27
33
|
|
|
34
|
+
/**
|
|
35
|
+
* The ones a profile gets whether it asked or not.
|
|
36
|
+
*
|
|
37
|
+
* `identity` is the exception and stays off this list, for the reason
|
|
38
|
+
* `ensureIdentityConnection` gives: a profile with no identity block has nothing
|
|
39
|
+
* for the surface to report, so registering a tool that answers "nothing
|
|
40
|
+
* declared" would spend instructions budget to say so. Everything else here
|
|
41
|
+
* reaches the owner's own material and is empty until they put something in it,
|
|
42
|
+
* which is ADR-050's whole argument — so a profile written before those existed
|
|
43
|
+
* gets them on the next command rather than needing five of its own.
|
|
44
|
+
*
|
|
45
|
+
* Ordered as `RESERVED_PROVIDER_IDS` is, so a repair reports in the order the
|
|
46
|
+
* template writes and a diff between the two reads as a diff.
|
|
47
|
+
*/
|
|
48
|
+
export const DEFAULT_SURFACES: readonly ReservedSurface[] = [
|
|
49
|
+
'memory',
|
|
50
|
+
'tasks',
|
|
51
|
+
'assets',
|
|
52
|
+
'skills',
|
|
53
|
+
'vault',
|
|
54
|
+
'setup',
|
|
55
|
+
];
|
|
56
|
+
|
|
28
57
|
/**
|
|
29
58
|
* What a repair did, split by what a caller does with each half.
|
|
30
59
|
*
|
|
@@ -162,14 +191,28 @@ function patternsIn(rules: unknown, now = Date.now()): string[] {
|
|
|
162
191
|
}
|
|
163
192
|
|
|
164
193
|
/**
|
|
165
|
-
* The
|
|
194
|
+
* The owner layer, which every profile is expected to have.
|
|
195
|
+
*
|
|
196
|
+
* Was `ensureSetupConnection`, and grew rather than gained siblings: the callers
|
|
197
|
+
* are the same three, and what changed is how many surfaces "a profile should be
|
|
198
|
+
* able to reach its own material" covers. Repairs are accumulated so a caller
|
|
199
|
+
* reports one list — five separate calls would print five near-identical blocks
|
|
200
|
+
* on the one upgrade where any of them fire.
|
|
166
201
|
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
* each of them says less than the name did.
|
|
202
|
+
* Each surface is still decided independently, so a profile that denied exactly
|
|
203
|
+
* one of them keeps that decision while the rest are repaired.
|
|
170
204
|
*/
|
|
171
|
-
export function
|
|
172
|
-
|
|
205
|
+
export function ensureOwnerLayer(document: ConfigDocument): SurfaceRepair {
|
|
206
|
+
const changes: string[] = [];
|
|
207
|
+
const granted: string[] = [];
|
|
208
|
+
|
|
209
|
+
for (const provider of DEFAULT_SURFACES) {
|
|
210
|
+
const repair = ensureReservedConnection(document, provider);
|
|
211
|
+
changes.push(...repair.changes);
|
|
212
|
+
granted.push(...repair.granted);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return { changes, granted };
|
|
173
216
|
}
|
|
174
217
|
|
|
175
218
|
/**
|
|
@@ -184,3 +227,64 @@ export function ensureSetupConnection(document: ConfigDocument): SurfaceRepair {
|
|
|
184
227
|
export function ensureIdentityConnection(document: ConfigDocument): SurfaceRepair {
|
|
185
228
|
return ensureReservedConnection(document, 'identity');
|
|
186
229
|
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Apply that repair across a workspace, saving and reporting what changed.
|
|
233
|
+
*
|
|
234
|
+
* Here rather than in `#deployments`, where it was, because `start` needs it as
|
|
235
|
+
* much as `deploy` does — more, in fact: `start` is the one command an existing
|
|
236
|
+
* install runs without being asked to, so it is the path by which a profile
|
|
237
|
+
* written before ADR-050 gets the layer at all. Two copies of a function that
|
|
238
|
+
* widens a policy is not a thing to have.
|
|
239
|
+
*
|
|
240
|
+
* **The caller scopes it**, and for `deploy` that is exactly the set being
|
|
241
|
+
* uploaded: a profile it sends is a profile the endpoint will serve, so
|
|
242
|
+
* repairing a narrower set would leave a served profile without the surfaces.
|
|
243
|
+
* Note what a `--profile` flag does not mean — it is the flag alone, so a
|
|
244
|
+
* profile resolved from the environment leaves it undefined and that reads as
|
|
245
|
+
* the whole workspace.
|
|
246
|
+
*
|
|
247
|
+
* *Which files are profiles* comes from `listProfiles`, never from an allowlist
|
|
248
|
+
* of what is safe to copy: that would happily hand over a committed
|
|
249
|
+
* `personal.example.yaml` or a nested `profiles/archive/old.yaml`, and this
|
|
250
|
+
* opens and validates what it is given — which once turned a template into a
|
|
251
|
+
* `ConfigError` aborting a deploy after provisioning had made cloud resources.
|
|
252
|
+
*
|
|
253
|
+
* A profile that cannot be read is warned about rather than fatal: the repair is
|
|
254
|
+
* a courtesy on the way past, and the caller's real work should still happen.
|
|
255
|
+
* Not silent, though — nothing else widens a policy without being asked.
|
|
256
|
+
*
|
|
257
|
+
* CLI-side by construction, like everything else in this file: a deployed
|
|
258
|
+
* revision holds `objectViewer` on `profiles/` (ADR-023) and could not write
|
|
259
|
+
* this even if the code let it.
|
|
260
|
+
*/
|
|
261
|
+
export async function repairOwnerLayer(
|
|
262
|
+
workspaceRoot: string,
|
|
263
|
+
profiles: readonly string[] | undefined,
|
|
264
|
+
): Promise<void> {
|
|
265
|
+
const wanted = profiles === undefined ? undefined : new Set(profiles);
|
|
266
|
+
|
|
267
|
+
for (const name of await listProfiles(workspaceRoot)) {
|
|
268
|
+
if (wanted !== undefined && !wanted.has(name)) continue;
|
|
269
|
+
|
|
270
|
+
try {
|
|
271
|
+
const document = await ConfigDocument.open(workspaceRoot, name);
|
|
272
|
+
const repair = ensureOwnerLayer(document);
|
|
273
|
+
if (!repaired(repair)) continue;
|
|
274
|
+
|
|
275
|
+
await document.save();
|
|
276
|
+
|
|
277
|
+
print(ok(`gave ${style.bold(name)} its own owner layer`));
|
|
278
|
+
for (const change of repairLines(repair)) print(` ${style.dim(change)}`);
|
|
279
|
+
print(
|
|
280
|
+
` ${style.dim('memory, tasks, assets, skills, vault and setup — your own material, no account behind any of them')}`,
|
|
281
|
+
);
|
|
282
|
+
} catch (error) {
|
|
283
|
+
print(
|
|
284
|
+
warn(
|
|
285
|
+
`could not give ${name} its owner layer: ${error instanceof Error ? error.message.split('\n')[0] : String(error)}`,
|
|
286
|
+
),
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
|
+
assetsAdd,
|
|
3
|
+
assetsGet,
|
|
4
|
+
assetsList,
|
|
5
|
+
assetsRemove,
|
|
2
6
|
memoryForget,
|
|
3
7
|
memoryGet,
|
|
4
8
|
memoryList,
|
|
@@ -11,18 +15,23 @@ import {
|
|
|
11
15
|
vaultKeyGenerate,
|
|
12
16
|
vaultList,
|
|
13
17
|
vaultRemove,
|
|
18
|
+
tasksAdd,
|
|
19
|
+
tasksGet,
|
|
20
|
+
tasksList,
|
|
21
|
+
tasksRemove,
|
|
22
|
+
tasksUpdate,
|
|
14
23
|
vaultSet,
|
|
15
24
|
type OwnerFlags,
|
|
16
25
|
} from './commands/owner.ts';
|
|
17
26
|
|
|
18
27
|
/**
|
|
19
|
-
* The
|
|
28
|
+
* The commands over the owner's own data: memory, tasks, assets, skills, vault.
|
|
20
29
|
*
|
|
21
30
|
* Split out of `main.ts` for the reason the budget in `src/architecture.test.ts`
|
|
22
|
-
* exists to find, rather than to satisfy a line count. These
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
31
|
+
* exists to find, rather than to satisfy a line count. These are one subject —
|
|
32
|
+
* what the owner put here themselves, as against what a provider holds on their
|
|
33
|
+
* behalf — they already live together in `commands/owner/`, and they are the
|
|
34
|
+
* only commands in the grammar sharing a flag shape of their own.
|
|
26
35
|
*
|
|
27
36
|
* `main.ts` keeps the grammar. This keeps one branch of it.
|
|
28
37
|
*/
|
|
@@ -49,6 +58,38 @@ export function dispatchOwner(
|
|
|
49
58
|
throw new Error(`Unknown: ${program} memory ${second}`);
|
|
50
59
|
}
|
|
51
60
|
|
|
61
|
+
case 'tasks':
|
|
62
|
+
switch (second) {
|
|
63
|
+
case 'list':
|
|
64
|
+
case undefined:
|
|
65
|
+
return tasksList(owner);
|
|
66
|
+
case 'get':
|
|
67
|
+
return tasksGet(rest[0], owner);
|
|
68
|
+
case 'add':
|
|
69
|
+
return tasksAdd(rest[0], owner);
|
|
70
|
+
case 'update':
|
|
71
|
+
return tasksUpdate(rest[0], owner);
|
|
72
|
+
case 'remove':
|
|
73
|
+
return tasksRemove(rest[0], owner);
|
|
74
|
+
default:
|
|
75
|
+
throw new Error(`Unknown: ${program} tasks ${second}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
case 'assets':
|
|
79
|
+
switch (second) {
|
|
80
|
+
case 'list':
|
|
81
|
+
case undefined:
|
|
82
|
+
return assetsList(owner);
|
|
83
|
+
case 'get':
|
|
84
|
+
return assetsGet(rest[0], owner);
|
|
85
|
+
case 'add':
|
|
86
|
+
return assetsAdd(rest[0], owner);
|
|
87
|
+
case 'remove':
|
|
88
|
+
return assetsRemove(rest[0], owner);
|
|
89
|
+
default:
|
|
90
|
+
throw new Error(`Unknown: ${program} assets ${second}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
52
93
|
case 'skills':
|
|
53
94
|
switch (second) {
|
|
54
95
|
case 'list':
|
|
@@ -85,9 +126,9 @@ export function dispatchOwner(
|
|
|
85
126
|
}
|
|
86
127
|
|
|
87
128
|
default:
|
|
88
|
-
// Unreachable: `main.ts` narrows to the
|
|
89
|
-
// so that adding
|
|
90
|
-
//
|
|
129
|
+
// Unreachable: `main.ts` narrows to the nouns above before calling. Kept
|
|
130
|
+
// so that adding one there and forgetting it here is a thrown error rather
|
|
131
|
+
// than a command that silently does nothing.
|
|
91
132
|
throw new Error(`Unknown: ${program} ${first}`);
|
|
92
133
|
}
|
|
93
134
|
}
|
package/src/cli/lanes.ts
CHANGED
|
@@ -18,7 +18,7 @@ import { version } from './version.ts';
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
const AREAS: Record<string, string> = {
|
|
21
|
-
link: 'a self-hostable MCP gateway for all your connections, memory,
|
|
21
|
+
link: 'a self-hostable MCP gateway for all your connections, memory, tasks, files, and secrets',
|
|
22
22
|
};
|
|
23
23
|
|
|
24
24
|
function areasUsage(): string {
|
package/src/cli/main.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { connect } from './commands/connect/index.ts';
|
|
2
2
|
import { connectCustom } from './commands/connect/custom/index.ts';
|
|
3
|
+
import { disconnect, relabel } from './commands/connection.ts';
|
|
3
4
|
import {
|
|
4
5
|
attachFile,
|
|
5
6
|
auditTail,
|
|
@@ -246,17 +247,20 @@ export async function run(argv: readonly string[]): Promise<void> {
|
|
|
246
247
|
if (second !== 'show' && second !== undefined) throw new Error(`Unknown: ${PROGRAM} config ${second}`);
|
|
247
248
|
return configShow(global);
|
|
248
249
|
|
|
249
|
-
//
|
|
250
|
+
// The owner's own data — one subject, dispatched together.
|
|
250
251
|
// `vault key generate` is synchronous, so this returns the result rather
|
|
251
252
|
// than testing it for truthiness.
|
|
252
253
|
case 'memory':
|
|
254
|
+
case 'tasks':
|
|
255
|
+
case 'assets':
|
|
253
256
|
case 'skills':
|
|
254
257
|
case 'vault':
|
|
255
258
|
return dispatchOwner(first, second, rest, owner, PROGRAM);
|
|
256
259
|
|
|
257
260
|
// Beside `memory` and `skills` because it is the question they raise next:
|
|
258
261
|
// those two say what is stored, and this says where it is kept. Not one of
|
|
259
|
-
// them, though — it takes its own flags rather than the owner set
|
|
262
|
+
// them, though — it takes its own flags rather than the owner set, and it
|
|
263
|
+
// moves those two only (ADR-041), not tasks or assets.
|
|
260
264
|
case 'knowledge':
|
|
261
265
|
switch (second) {
|
|
262
266
|
case 'show':
|
|
@@ -273,7 +277,7 @@ export async function run(argv: readonly string[]): Promise<void> {
|
|
|
273
277
|
case 'plan':
|
|
274
278
|
return plan(global);
|
|
275
279
|
case 'doctor':
|
|
276
|
-
return doctor({ ...global, json });
|
|
280
|
+
return doctor({ ...global, json, fix: flags['fix'] === true });
|
|
277
281
|
case 'status':
|
|
278
282
|
return status({ ...global, json });
|
|
279
283
|
case 'outputs':
|
|
@@ -329,6 +333,21 @@ export async function run(argv: readonly string[]): Promise<void> {
|
|
|
329
333
|
default:
|
|
330
334
|
throw new Error(`Unknown: ${PROGRAM} mcp ${second}`);
|
|
331
335
|
}
|
|
336
|
+
case 'disconnect':
|
|
337
|
+
return disconnect(second, {
|
|
338
|
+
...global,
|
|
339
|
+
yes: flags['yes'] === true,
|
|
340
|
+
keepCredential: flags['keep-credential'] === true,
|
|
341
|
+
json: flags['json'] === true,
|
|
342
|
+
});
|
|
343
|
+
// The new label is joined rather than taken as `rest[0]`, so an unquoted
|
|
344
|
+
// multi-word name works: `relabel gmail.main Work Mail` is what someone
|
|
345
|
+
// types before they think about quoting, and refusing it teaches nothing.
|
|
346
|
+
case 'relabel':
|
|
347
|
+
return relabel(second, rest.length > 0 ? rest.join(' ') : undefined, {
|
|
348
|
+
...global,
|
|
349
|
+
json: flags['json'] === true,
|
|
350
|
+
});
|
|
332
351
|
case 'start':
|
|
333
352
|
return start({
|
|
334
353
|
...global,
|
|
@@ -41,5 +41,5 @@ export const PROVIDER_MARKS: Readonly<Record<string, string>> = {
|
|
|
41
41
|
linear: 'M2.886 4.18A11.982 11.982 0 0 1 11.99 0C18.624 0 24 5.376 24 12.009c0 3.64-1.62 6.903-4.18 9.105L2.887 4.18ZM1.817 5.626l16.556 16.556c-.524.33-1.075.62-1.65.866L.951 7.277c.247-.575.537-1.126.866-1.65ZM.322 9.163l14.515 14.515c-.71.172-1.443.282-2.195.322L0 11.358a12 12 0 0 1 .322-2.195Zm-.17 4.862 9.823 9.824a12.02 12.02 0 0 1-9.824-9.824Z',
|
|
42
42
|
notion: 'M4.459 4.208c.746.606 1.026.56 2.428.466l13.215-.793c.28 0 .047-.28-.046-.326L17.86 1.968c-.42-.326-.981-.7-2.055-.607L3.01 2.295c-.466.046-.56.28-.374.466zm.793 3.08v13.904c0 .747.373 1.027 1.214.98l14.523-.84c.841-.046.935-.56.935-1.167V6.354c0-.606-.233-.933-.748-.887l-15.177.887c-.56.047-.747.327-.747.933zm14.337.745c.093.42 0 .84-.42.888l-.7.14v10.264c-.608.327-1.168.514-1.635.514-.748 0-.935-.234-1.495-.933l-4.577-7.186v6.952L12.21 19s0 .84-1.168.84l-3.222.186c-.093-.186 0-.653.327-.746l.84-.233V9.854L7.822 9.76c-.094-.42.14-1.026.793-1.073l3.456-.233 4.764 7.279v-6.44l-1.215-.139c-.093-.514.28-.887.747-.933zM1.936 1.035l13.31-.98c1.634-.14 2.055-.047 3.082.7l4.249 2.986c.7.513.934.653.934 1.213v16.378c0 1.026-.373 1.634-1.68 1.726l-15.458.934c-.98.047-1.448-.093-1.962-.747l-3.129-4.06c-.56-.747-.793-1.306-.793-1.96V2.667c0-.839.374-1.54 1.447-1.632z',
|
|
43
43
|
sheets: 'M11.318 12.545H7.91v-1.909h3.41v1.91zM14.728 0v6h6l-6-6zm1.363 10.636h-3.41v1.91h3.41v-1.91zm0 3.273h-3.41v1.91h3.41v-1.91zM20.727 6.5v15.864c0 .904-.732 1.636-1.636 1.636H4.909a1.636 1.636 0 0 1-1.636-1.636V1.636C3.273.732 4.005 0 4.909 0h9.318v6.5h6.5zm-3.273 2.773H6.545v7.909h10.91v-7.91zm-6.136 4.636H7.91v1.91h3.41v-1.91z',
|
|
44
|
-
|
|
44
|
+
google_tasks: 'M11.383.617C5.097.617 0 5.714 0 12c0 6.286 5.097 11.383 11.383 11.383 6.286 0 11.38-5.097 11.38-11.383a11.34 11.34 0 0 0-.878-4.389l-3.203 3.203c.062.387.1.782.1 1.186a7.398 7.398 0 1 1-7.4-7.398c1.499 0 2.889.448 4.054 1.214l2.857-2.857a11.325 11.325 0 0 0-6.91-2.342zm9.674.756c-.292 0-.583.112-.805.334-2.97 2.965-5.934 5.934-8.9 8.902L9.596 8.854a1.139 1.139 0 0 0-1.61 0l-1.775 1.773a1.139 1.139 0 0 0 0 1.61l4.166 4.163a1.421 1.421 0 0 0 2.012 0L23.666 5.121a1.136 1.136 0 0 0 0-1.61l-1.805-1.804a1.136 1.136 0 0 0-.804-.334z',
|
|
45
45
|
};
|