@lanes-sh/link 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +20 -9
  2. package/instructions/agents/lanes-link-scout.md +14 -3
  3. package/instructions/skills/lanes-link/SKILL.md +80 -3
  4. package/package.json +2 -2
  5. package/src/cli/argv.ts +7 -0
  6. package/src/cli/commands/connect/index.ts +9 -6
  7. package/src/cli/commands/connection.ts +298 -0
  8. package/src/cli/commands/mcp/list.ts +123 -29
  9. package/src/cli/commands/operate/inspect.ts +37 -20
  10. package/src/cli/commands/operate/serve.ts +21 -0
  11. package/src/cli/commands/owner/assets.ts +132 -0
  12. package/src/cli/commands/owner/shared.ts +28 -4
  13. package/src/cli/commands/owner/tasks.ts +194 -0
  14. package/src/cli/commands/owner.ts +9 -4
  15. package/src/cli/config-edit.ts +33 -7
  16. package/src/cli/config-repair.ts +115 -11
  17. package/src/cli/dispatch-owner.ts +49 -8
  18. package/src/cli/lanes.ts +1 -1
  19. package/src/cli/main.ts +26 -3
  20. package/src/cli/provider-marks.ts +1 -1
  21. package/src/cli/runtime/registry.ts +10 -2
  22. package/src/cli/selection.ts +14 -0
  23. package/src/cli/usage.ts +18 -2
  24. package/src/connectivity/mail/attachments.ts +5 -1
  25. package/src/connectivity/mail/index.ts +6 -1
  26. package/src/connectivity/manifest/provider.ts +15 -2
  27. package/src/deployments/deploy.ts +3 -2
  28. package/src/deployments/prepare.ts +1 -1
  29. package/src/deployments/servable.ts +1 -1
  30. package/src/deployments/upload.ts +0 -53
  31. package/src/profile/load.ts +46 -0
  32. package/src/providers/assets/provider.ts +337 -0
  33. package/src/providers/assets/store.ts +167 -0
  34. package/src/providers/bunq/hints.ts +3 -1
  35. package/src/providers/bunq/redact.ts +13 -2
  36. package/src/providers/bunq/specs/bunq.v1.json +20 -1
  37. package/src/providers/bunq/specs/vendor.ts +59 -1
  38. package/src/providers/google/index.ts +1 -1
  39. package/src/providers/google/tasks/index.ts +3 -3
  40. package/src/providers/google/tasks/redact.ts +21 -11
  41. package/src/providers/index.ts +3 -3
  42. package/src/providers/owner.ts +39 -19
  43. package/src/providers/setup/plan.ts +17 -1
  44. package/src/providers/shared/vendor-operations.ts +81 -0
  45. package/src/providers/tasks/provider.ts +370 -0
  46. package/src/providers/tasks/store.ts +248 -0
  47. package/src/server/mcp/build.ts +1 -1
  48. package/src/server/mcp/instructions.ts +67 -8
@@ -0,0 +1,194 @@
1
+ import { ConfigError } from '#profile';
2
+ import { scopeNamespace } from '#dispatch';
3
+ import { scopeBlobStore, type BlobStore } from '#stores/blobs';
4
+ import { ACTIVE_STATUSES, TASK_STATUSES, taskStorage, type TaskStatus } from '#providers/owner.ts';
5
+ import { heading, ok, print, style, table } from '../../output.ts';
6
+ import type { Runtime } from '../../runtime.ts';
7
+ import {
8
+ agreed,
9
+ optionalStdin,
10
+ ownerConnection,
11
+ required,
12
+ withRuntime,
13
+ type OwnerFlags,
14
+ } from './shared.ts';
15
+
16
+ /** `lanes link tasks` — what the owner has to do. */
17
+
18
+ export async function tasksList(flags: OwnerFlags): Promise<void> {
19
+ await withRuntime(flags, async (runtime) => {
20
+ const store = tasksStore(runtime, flags);
21
+
22
+ // The same default the `tasks.list` capability applies, and for the same
23
+ // reason: the question is what is outstanding, and a list that grows forever
24
+ // is one nobody reads. `--status all` is the escape hatch.
25
+ const wanted = statusFilter(flags.status);
26
+ const tasks = (await taskStorage.all(store)).filter(
27
+ (task) =>
28
+ (wanted === null || wanted.has(task.status)) && (!flags.tag || task.tags.includes(flags.tag)),
29
+ );
30
+
31
+ heading(`Tasks (${tasks.length}${wanted === null ? '' : ' outstanding'})`);
32
+ if (tasks.length === 0) {
33
+ print(style.dim(' none — add one with: lanes link tasks add <title>'));
34
+ return;
35
+ }
36
+
37
+ table(
38
+ tasks.map((task) => [
39
+ ` ${task.id}`,
40
+ task.status,
41
+ task.title,
42
+ task.due ? style.dim(`due ${task.due}`) : '',
43
+ task.tags.length > 0 ? style.dim(task.tags.join(', ')) : '',
44
+ ]),
45
+ );
46
+ });
47
+ }
48
+
49
+ export async function tasksGet(id: string | undefined, flags: OwnerFlags): Promise<void> {
50
+ const taskId = required(id, 'lanes link tasks get <id>');
51
+
52
+ await withRuntime(flags, async (runtime) => {
53
+ const task = await taskStorage.read(tasksStore(runtime, flags), taskId);
54
+ if (!task) throw new ConfigError(`No task "${taskId}" in this profile.`);
55
+
56
+ print('');
57
+ print(` ${style.bold(task.title)}`);
58
+ print(style.dim(` ${task.status}${task.due ? ` due ${task.due}` : ''}`));
59
+ if (task.tags.length > 0) print(style.dim(` ${task.tags.join(', ')}`));
60
+ if (task.body.length > 0) {
61
+ print('');
62
+ print(task.body);
63
+ }
64
+ });
65
+ }
66
+
67
+ /**
68
+ * `lanes link tasks add <title>` — the title on argv, notes optional on stdin.
69
+ *
70
+ * Unlike `memory write`, the title is the argument and the body is optional: a
71
+ * task is usually one line, and demanding a heredoc to write "chase the invoice"
72
+ * would make the common case the awkward one. So `optionalStdin` rather than
73
+ * `readStdin` — see its docstring for what refusing an empty pipe here broke.
74
+ */
75
+ export async function tasksAdd(title: string | undefined, flags: OwnerFlags): Promise<void> {
76
+ const given = required(title, 'lanes link tasks add <title> (notes on stdin, optional)');
77
+ const notes = await optionalStdin();
78
+
79
+ await withRuntime(flags, async (runtime) => {
80
+ const store = tasksStore(runtime, flags);
81
+ const id = taskStorage.slugify(given);
82
+ const existing = await taskStorage.read(store, id);
83
+ const now = new Date().toISOString();
84
+
85
+ await taskStorage.write(store, {
86
+ id,
87
+ title: given,
88
+ status: assertStatus(flags.status) ?? 'open',
89
+ tags: flags.tag ? [flags.tag] : (existing?.tags ?? []),
90
+ ...(flags.due ? { due: flags.due } : {}),
91
+ createdAt: existing?.createdAt ?? now,
92
+ updatedAt: now,
93
+ body: notes,
94
+ });
95
+
96
+ print(ok(`${existing ? 'replaced' : 'added'} task ${style.bold(id)}`));
97
+ });
98
+ }
99
+
100
+ /**
101
+ * `lanes link tasks update <id> --status done`.
102
+ *
103
+ * Omitted flags leave their fields alone, which is what makes this the way to
104
+ * close a task rather than delete it: the record of having done it is the useful
105
+ * part, and `--status done` keeps everything else.
106
+ */
107
+ export async function tasksUpdate(id: string | undefined, flags: OwnerFlags): Promise<void> {
108
+ const taskId = required(id, 'lanes link tasks update <id> --status <status>');
109
+
110
+ await withRuntime(flags, async (runtime) => {
111
+ const store = tasksStore(runtime, flags);
112
+ const existing = await taskStorage.read(store, taskId);
113
+ if (!existing) throw new ConfigError(`No task "${taskId}" in this profile.`);
114
+
115
+ const status = assertStatus(flags.status);
116
+ if (!status && !flags.title && !flags.due && !flags.tag) {
117
+ throw new ConfigError(
118
+ `Nothing to change. Pass --status, --title, --due or --tag.\n` +
119
+ ` statuses: ${TASK_STATUSES.join(', ')}`,
120
+ );
121
+ }
122
+
123
+ // `due` is taken off the existing record rather than spread and overwritten,
124
+ // because spreading cannot remove a key — the same trap the provider's
125
+ // `update` documents.
126
+ const { due: previous, ...rest } = existing;
127
+ const due = flags.due === '' ? undefined : (flags.due ?? previous);
128
+
129
+ await taskStorage.write(store, {
130
+ ...rest,
131
+ title: flags.title ?? existing.title,
132
+ status: status ?? existing.status,
133
+ tags: flags.tag ? [flags.tag] : existing.tags,
134
+ ...(due ? { due } : {}),
135
+ updatedAt: new Date().toISOString(),
136
+ });
137
+
138
+ print(ok(`updated task ${style.bold(taskId)} — now ${status ?? existing.status}`));
139
+ });
140
+ }
141
+
142
+ export async function tasksRemove(id: string | undefined, flags: OwnerFlags): Promise<void> {
143
+ const taskId = required(id, 'lanes link tasks remove <id>');
144
+
145
+ await withRuntime(flags, async (runtime) => {
146
+ const store = tasksStore(runtime, flags);
147
+ const task = await taskStorage.read(store, taskId);
148
+ if (!task) throw new ConfigError(`No task "${taskId}" in this profile.`);
149
+
150
+ print(` ${style.bold(task.id)} ${task.status} ${task.title}`);
151
+ print(
152
+ style.dim(' deleting loses the record that it happened — "update --status done" keeps it'),
153
+ );
154
+ if (!(await agreed(flags, 'Delete this task?'))) return;
155
+
156
+ await store.delete(taskStorage.key(taskId));
157
+ print(ok(`deleted task ${style.bold(taskId)}`));
158
+ });
159
+ }
160
+
161
+ /**
162
+ * `--status` as a filter: a named one, `all`, or the outstanding set by default.
163
+ *
164
+ * `null` means every status. Returning a set rather than a predicate so the
165
+ * heading can say whether it narrowed.
166
+ */
167
+ function statusFilter(raw: string | undefined): Set<TaskStatus> | null {
168
+ if (raw === 'all') return null;
169
+ if (raw === undefined) return new Set(ACTIVE_STATUSES);
170
+ return new Set([assertStatus(raw)!]);
171
+ }
172
+
173
+ function assertStatus(raw: string | undefined): TaskStatus | undefined {
174
+ if (raw === undefined) return undefined;
175
+ if (!(TASK_STATUSES as readonly string[]).includes(raw)) {
176
+ throw new ConfigError(
177
+ `Unknown status "${raw}". One of: ${TASK_STATUSES.join(', ')}` +
178
+ '\n (or "all", when filtering a listing)',
179
+ );
180
+ }
181
+ return raw as TaskStatus;
182
+ }
183
+
184
+ /**
185
+ * The blob namespace core would scope this provider to.
186
+ *
187
+ * Built from `scopeNamespace` and `scopeBlobStore` — the same two functions
188
+ * `buildProviderContext` uses — rather than from a path spelled out again, so the
189
+ * CLI cannot address a different directory from the provider.
190
+ */
191
+ export function tasksStore(runtime: Runtime, flags: OwnerFlags): BlobStore {
192
+ const connection = ownerConnection(runtime.config, 'tasks', flags);
193
+ return scopeBlobStore(runtime.storage, scopeNamespace('tasks', connection));
194
+ }
@@ -1,5 +1,6 @@
1
1
  /**
2
- * `lanes link memory`, `lanes link skills`, `lanes link vault` — the owner layer's control plane.
2
+ * `lanes link memory`, `tasks`, `assets`, `skills`, `vault` — the owner layer's
3
+ * control plane.
3
4
  *
4
5
  * The layer shipped in M4 with no CLI at all, so the two stores holding the
5
6
  * owner's *own* data were reachable only by an agent, and the one thing that
@@ -20,9 +21,9 @@
20
21
  * both sides now: from here because this is the owner's control plane, and over
21
22
  * MCP because ADR-014 §1 decided a policy-gated grant beats a missing path.
22
23
  *
23
- * One noun per file — `memory.ts`, `skills.ts`, `vault.ts` — over the shape all
24
- * three share in `shared.ts`: the flag type, the open-announce-act-close
25
- * wrapper, connection resolution, and the two prompts.
24
+ * One noun per file — `memory.ts`, `tasks.ts`, `assets.ts`, `skills.ts`,
25
+ * `vault.ts` — over the shape they all share in `shared.ts`: the flag type, the
26
+ * open-announce-act-close wrapper, connection resolution, and the two prompts.
26
27
  */
27
28
 
28
29
  export {
@@ -33,6 +34,10 @@ export {
33
34
  memoryWrite,
34
35
  } from './owner/memory.ts';
35
36
 
37
+ export { tasksAdd, tasksGet, tasksList, tasksRemove, tasksUpdate } from './owner/tasks.ts';
38
+
39
+ export { assetsAdd, assetsGet, assetsList, assetsRemove } from './owner/assets.ts';
40
+
36
41
  export { skillsAdd, skillsList, skillsRemove, skillsShow } from './owner/skills.ts';
37
42
 
38
43
  export {
@@ -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
- # "setup" holds no account. It lets an agent see what is connected here and what
268
- # connecting something else would take, so it can tell you the command to run
269
- # rather than guess at one. It is read-only: nothing it offers writes config,
270
- # stores a credential, signs in, or changes what is permitted — those stay in
271
- # this CLI (ADR-007, ADR-019). Delete this entry and the allow line below to
272
- # remove it entirely.
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
  }
@@ -1,4 +1,6 @@
1
- import type { ConfigDocument } from './config-edit.ts';
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
- * Two providers hold no account and are therefore invisible without a
13
- * connection row nobody would think to write: `setup`, which describes what is
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 two that look alike.
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 `setup` surface, which every profile is expected to have.
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
- * A named wrapper rather than a call site passing `'setup'`, because three
168
- * callers say it and reading `ensureReservedConnection(document, 'setup')` at
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 ensureSetupConnection(document: ConfigDocument): SurfaceRepair {
172
- return ensureReservedConnection(document, 'setup');
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 three commands over the owner's own data: memory, skills, and the vault.
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 three are one
23
- * subject — what the owner put here themselves, as against what a provider
24
- * holds on their behalf — they already live together in `commands/owner/`, and
25
- * they are the only commands in the grammar sharing a flag shape of their own.
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 three above before calling. Kept
89
- // so that adding a fourth case there and forgetting it here is a thrown
90
- // error rather than a command that silently does nothing.
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, skills, and secrets',
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
- // memory, skills and vault — one subject, dispatched together.
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':
@@ -321,10 +325,29 @@ export async function run(argv: readonly string[]): Promise<void> {
321
325
  return mcpStdio({ ...global, ...(flags['only'] === true ? { only: true } : {}) });
322
326
  case 'list':
323
327
  case undefined:
324
- return mcpList({ name: text(flags, 'name'), scope: text(flags, 'scope') });
328
+ return mcpList({
329
+ name: text(flags, 'name'),
330
+ scope: text(flags, 'scope'),
331
+ json: flags['json'] === true,
332
+ });
325
333
  default:
326
334
  throw new Error(`Unknown: ${PROGRAM} mcp ${second}`);
327
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
+ });
328
351
  case 'start':
329
352
  return start({
330
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
- 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',
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
  };
@@ -7,12 +7,14 @@ import { loadProfileProviders } from '#providers/custom/index.ts';
7
7
  import { loadProfileSkills, type LoadedSkill } from '#providers/skills/store.ts';
8
8
  import { exampleProvider } from '#providers/example/provider.ts';
9
9
  import {
10
+ assetsProvider,
10
11
  createIdentityProvider,
11
12
  createMemoryVaultStore,
12
13
  createSetupProvider,
13
14
  createSkillsProvider,
14
15
  createVaultProvider,
15
16
  memoryProvider,
17
+ tasksProvider,
16
18
  type IdentityProviderOptions,
17
19
  type SetupProviderOptions,
18
20
  type VaultStore,
@@ -84,8 +86,8 @@ export interface OwnerLayerOptions {
84
86
  * Statically imported for now; the registry does not care where a manifest came
85
87
  * from, which is what lets workspace YAML register alongside these.
86
88
  *
87
- * `allowReserved` is what admits `memory`, `skills`, `vault`, `setup`, and
88
- * `identity`. The guard
89
+ * `allowReserved` is what admits `memory`, `tasks`, `assets`, `skills`, `vault`,
90
+ * `setup`, and `identity`. The guard
89
91
  * stays rather than being retired: it exists so a *third-party* provider cannot
90
92
  * claim a namespace whose policy rules would then silently mean something else,
91
93
  * and that reason survives the owner layer shipping. Only this one construction
@@ -95,7 +97,13 @@ export function buildRegistry(owner: OwnerLayerOptions = {}): ProviderRegistry {
95
97
  const registry = new ProviderRegistry({ allowReserved: true });
96
98
 
97
99
  registry.register(exampleProvider);
100
+ // The three that hold what the owner keeps. All module-level constants rather
101
+ // than factories: each reads and writes through `context.storage`, which core
102
+ // scopes per call, so there is nothing to hand in at construction time and
103
+ // nothing for `OwnerLayerOptions` to carry.
98
104
  registry.register(memoryProvider);
105
+ registry.register(tasksProvider);
106
+ registry.register(assetsProvider);
99
107
  registry.register(skillsProviderFor(owner));
100
108
  registry.register(
101
109
  createVaultProvider({