@1agh/maude 0.47.0 → 0.49.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 (58) hide show
  1. package/README.md +7 -6
  2. package/apps/studio/acp/bridge.ts +8 -1
  3. package/apps/studio/acp/plugin-bootstrap.ts +15 -1
  4. package/apps/studio/api.ts +18 -2
  5. package/apps/studio/assets-s3.ts +291 -0
  6. package/apps/studio/build.ts +1 -1
  7. package/apps/studio/client/export-center.jsx +7 -6
  8. package/apps/studio/client/github.js +11 -4
  9. package/apps/studio/collab/origins.ts +150 -0
  10. package/apps/studio/collab/protocol.ts +36 -9
  11. package/apps/studio/collab/room.ts +124 -0
  12. package/apps/studio/context.ts +9 -0
  13. package/apps/studio/dist/client.bundle.js +282 -282
  14. package/apps/studio/dist/comment-mount.js +2 -2
  15. package/apps/studio/dist/runtime/REMOTION-LICENSE.md +1 -1
  16. package/apps/studio/examples/perf-100-artboards.tsx +1 -1
  17. package/apps/studio/exporters/pdf.ts +35 -1
  18. package/apps/studio/git/service.ts +4 -1
  19. package/apps/studio/http.ts +45 -0
  20. package/apps/studio/paths.ts +37 -1
  21. package/apps/studio/server.ts +75 -2
  22. package/apps/studio/sync/autocommit.ts +299 -0
  23. package/apps/studio/sync/doc-name.ts +228 -0
  24. package/apps/studio/sync/index.ts +95 -1
  25. package/apps/studio/sync/workspace-signin.ts +301 -0
  26. package/apps/studio/test/acp-plugin-bootstrap.test.ts +34 -0
  27. package/apps/studio/test/acp-session-plugins.test.ts +6 -0
  28. package/apps/studio/test/assets-s3.test.ts +249 -0
  29. package/apps/studio/test/canvas-origin-gate.test.ts +7 -0
  30. package/apps/studio/test/collab-origin-gate.test.ts +323 -0
  31. package/apps/studio/test/exporters/pdf.test.ts +33 -1
  32. package/apps/studio/test/sync-autocommit.test.ts +334 -0
  33. package/apps/studio/test/sync-doc-name.test.ts +281 -0
  34. package/apps/studio/test/workspace-containment.test.ts +258 -0
  35. package/apps/studio/test/workspace-signin.test.ts +270 -0
  36. package/apps/studio/use-collab.tsx +28 -1
  37. package/apps/studio/workspace-mode.ts +210 -0
  38. package/apps/studio/ws.ts +11 -2
  39. package/cli/bin/maude.mjs +1 -0
  40. package/cli/commands/hub-workspace.mjs +341 -0
  41. package/cli/commands/hub.mjs +325 -3
  42. package/cli/commands/hub.test.mjs +14 -1
  43. package/cli/commands/init.mjs +80 -3
  44. package/cli/commands/kg.mjs +368 -0
  45. package/cli/commands/kg.test.mjs +118 -0
  46. package/cli/lib/cell-plan.mjs +302 -0
  47. package/cli/lib/cell-plan.test.mjs +225 -0
  48. package/cli/lib/ddr-to-kgai.mjs +648 -0
  49. package/cli/lib/ddr-to-kgai.test.mjs +99 -0
  50. package/cli/lib/flow-design-integration.test.mjs +2 -2
  51. package/cli/lib/gitignore-block.mjs +16 -1
  52. package/cli/lib/plugin-name-namespace.test.mjs +71 -0
  53. package/cli/lib/workspace-plan.mjs +422 -0
  54. package/cli/lib/workspace-plan.test.mjs +223 -0
  55. package/package.json +8 -8
  56. package/plugins/design/dependencies.json +17 -0
  57. package/plugins/flow/.claude-plugin/config.schema.json +66 -0
  58. package/plugins/flow/dependencies.json +17 -0
@@ -0,0 +1,223 @@
1
+ // Cloud Phase 4 Task 1 — the workspace-up planning layer.
2
+ //
3
+ // A provisioner's real failure modes are decisions, not I/O: a bad domain
4
+ // rendering a broken Caddyfile, a missing no-expiry rule quietly scheduling
5
+ // someone's media for deletion, an "it worked!" printed before anything
6
+ // round-tripped. All testable without a VPS, which is why they live here.
7
+
8
+ import assert from 'node:assert/strict';
9
+ import { test } from 'node:test';
10
+
11
+ import {
12
+ envEntries,
13
+ operatorDuties,
14
+ renderCaddyfile,
15
+ renderCompose,
16
+ renderEnv,
17
+ validateWorkspaceConfig,
18
+ verificationPlan,
19
+ } from './workspace-plan.mjs';
20
+
21
+ const BASE = {
22
+ domain: 'design.acme.com',
23
+ acmeEmail: 'ops@acme.com',
24
+ adminEmail: 'alice@acme.com',
25
+ };
26
+ const S3 = {
27
+ endpoint: 'https://acct.r2.cloudflarestorage.com',
28
+ bucket: 'acme-design-assets',
29
+ accessKeyId: 'AKIA',
30
+ secretAccessKey: 'secret',
31
+ };
32
+ const ok = (raw) => {
33
+ const r = validateWorkspaceConfig(raw);
34
+ assert.deepEqual(r.errors, [], `expected valid: ${r.errors.join('; ')}`);
35
+ return r.config;
36
+ };
37
+
38
+ // ------------------------------------------------------------- validation
39
+
40
+ test('a valid config normalizes rather than nitpicking', () => {
41
+ const cfg = ok({ ...BASE, domain: 'HTTPS://Design.Acme.com/', adminEmail: 'Alice@Acme.com' });
42
+ assert.equal(cfg.domain, 'design.acme.com');
43
+ assert.equal(cfg.adminEmail, 'alice@acme.com');
44
+ assert.equal(cfg.imageTag, 'latest');
45
+ assert.equal(cfg.seedRepo, null);
46
+ });
47
+
48
+ test('EVERY problem is reported at once, not the first one', () => {
49
+ // Someone filling this in wants to fix everything in one pass, not play
50
+ // whack-a-mole with a wizard.
51
+ const res = validateWorkspaceConfig({ domain: 'not a domain', acmeEmail: 'nope' });
52
+ assert.equal(res.ok, false);
53
+ assert.ok(res.errors.length >= 3, `expected several errors, got ${res.errors.length}`);
54
+ assert.ok(res.errors.some((e) => e.includes('domain')));
55
+ assert.ok(res.errors.some((e) => e.includes('acmeEmail')));
56
+ assert.ok(res.errors.some((e) => e.includes('adminEmail')));
57
+ });
58
+
59
+ test('a bare hostname without a dot is refused', () => {
60
+ // "localhost" or "hub" would render a Caddyfile that can never get a cert.
61
+ const res = validateWorkspaceConfig({ ...BASE, domain: 'localhost' });
62
+ assert.equal(res.ok, false);
63
+ assert.ok(res.errors.some((e) => /domain/.test(e)));
64
+ });
65
+
66
+ test('a short admin password is refused before anything is written', () => {
67
+ const res = validateWorkspaceConfig({ ...BASE, adminPassword: 'short' });
68
+ assert.equal(res.ok, false);
69
+ assert.ok(res.errors.some((e) => /at least 12/.test(e)));
70
+ });
71
+
72
+ test('partial S3 config is an error, never a half-configured lane', () => {
73
+ const res = validateWorkspaceConfig({ ...BASE, s3: { bucket: 'b' } });
74
+ assert.equal(res.ok, false);
75
+ assert.ok(res.errors.some((e) => e.includes('s3.endpoint')));
76
+ assert.ok(res.errors.some((e) => e.includes('s3.accessKeyId')));
77
+ });
78
+
79
+ test('an invalid bucket name is caught here, not by the provider later', () => {
80
+ assert.equal(
81
+ validateWorkspaceConfig({ ...BASE, s3: { ...S3, bucket: 'A_Bad_Bucket' } }).ok,
82
+ false
83
+ );
84
+ });
85
+
86
+ test('a seed repo must look like a git URL', () => {
87
+ assert.equal(validateWorkspaceConfig({ ...BASE, seedRepo: 'my-folder' }).ok, false);
88
+ assert.equal(
89
+ ok({ ...BASE, seedRepo: 'git@github.com:acme/design.git' }).seedRepo,
90
+ 'git@github.com:acme/design.git'
91
+ );
92
+ assert.equal(ok({ ...BASE, seedRepo: ' ' }).seedRepo, null, 'blank means start fresh');
93
+ });
94
+
95
+ test('devMinio supplies its own credentials and marks them as dev', () => {
96
+ const cfg = ok({ ...BASE, devMinio: true });
97
+ assert.equal(cfg.s3.dev, true);
98
+ assert.equal(cfg.s3.endpoint, 'http://minio:9000');
99
+ });
100
+
101
+ // ------------------------------------------------------------------- env
102
+
103
+ test('.env carries the trusted-proxy setting UNCONDITIONALLY', () => {
104
+ // Caddy fronts the hub. Without this every client shares one rate-limit
105
+ // bucket and one attacker's login flood limits everybody (DDR-194 §4).
106
+ const entries = envEntries(ok(BASE), { hubSecret: 'deadbeef' });
107
+ const byKey = Object.fromEntries(entries.map((e) => [e.key, e.value]));
108
+ assert.ok(byKey.HUB_TRUSTED_PROXIES.includes('172.16.0.0/12'));
109
+ assert.equal(byKey.HUB_WORKSPACE_MODE, '1');
110
+ assert.equal(byKey.MAUDE_WORKSPACE_MODE, '1', 'the containment invariant must be on');
111
+ });
112
+
113
+ test('.env includes S3 only when configured', () => {
114
+ const without = envEntries(ok(BASE), { hubSecret: 'x' }).map((e) => e.key);
115
+ assert.ok(!without.includes('MAUDE_S3_BUCKET'));
116
+ const with3 = envEntries(ok({ ...BASE, s3: S3 }), { hubSecret: 'x' }).map((e) => e.key);
117
+ assert.ok(with3.includes('MAUDE_S3_BUCKET'));
118
+ assert.ok(with3.includes('MAUDE_S3_SECRET_ACCESS_KEY'));
119
+ });
120
+
121
+ test('renderEnv announces that it holds secrets', () => {
122
+ const text = renderEnv(
123
+ envEntries(ok(BASE), { hubSecret: 'sekrit', adminPassword: 'a-good-one-12' })
124
+ );
125
+ assert.match(text, /Contains SECRETS/);
126
+ assert.match(text, /Mode 0600, never committed/);
127
+ assert.match(text, /^HUB_SECRET=sekrit$/m);
128
+ // The bootstrap password is flagged as temporary rather than left to linger.
129
+ assert.match(text, /change it after, then remove this line/);
130
+ });
131
+
132
+ // --------------------------------------------------------------- compose
133
+
134
+ test('MinIO is behind a PROFILE, not a comment', () => {
135
+ // A commented-out service is one somebody uncomments in production.
136
+ const yaml = renderCompose(ok({ ...BASE, devMinio: true }));
137
+ assert.match(yaml, /profiles: \["dev"\]/);
138
+ assert.match(yaml, /DEV ONLY/);
139
+ assert.ok(!renderCompose(ok(BASE)).includes('minio'), 'no MinIO without devMinio');
140
+ });
141
+
142
+ test('the hub is exposed, never published — Caddy is the only way in', () => {
143
+ const yaml = renderCompose(ok({ ...BASE, s3: S3 }));
144
+ const hubBlock = yaml.slice(yaml.indexOf(' hub:'), yaml.indexOf(' caddy:'));
145
+ assert.match(hubBlock, /expose:/);
146
+ assert.ok(!/^\s+ports:/m.test(hubBlock), 'the hub must not publish a port to the host');
147
+ });
148
+
149
+ test('compose passes every configured secret by reference, never inline', () => {
150
+ const yaml = renderCompose(ok({ ...BASE, s3: S3 }));
151
+ assert.match(yaml, /MAUDE_S3_SECRET_ACCESS_KEY: \$\{MAUDE_S3_SECRET_ACCESS_KEY\}/);
152
+ assert.ok(!yaml.includes('secret'), 'no literal secret value may appear in compose');
153
+ });
154
+
155
+ test('Caddy forwards the client address, or per-client rate limiting cannot work', () => {
156
+ const caddy = renderCaddyfile(ok(BASE));
157
+ assert.match(caddy, /header_up X-Forwarded-For \{remote_host\}/);
158
+ assert.match(caddy, /reverse_proxy hub:1234/);
159
+ assert.match(caddy, /\{\$PUBLIC_DOMAIN\}/);
160
+ });
161
+
162
+ // ---------------------------------------------------------- verification
163
+
164
+ test('the plan verifies a ROUND TRIP, not just liveness', () => {
165
+ // A provisioner that prints a URL without proving a round-trip has told the
166
+ // operator something it does not know.
167
+ const ids = verificationPlan(ok(BASE)).map((s) => s.id);
168
+ assert.ok(ids.includes('canvas-roundtrip'));
169
+ assert.ok(ids.includes('git-commit'));
170
+ assert.ok(ids.includes('user-signin'));
171
+ assert.ok(ids.includes('restore-drill'));
172
+ });
173
+
174
+ test('with object storage it also checks that nothing will EXPIRE the media', () => {
175
+ // The quiet catastrophe: a lifecycle rule on assets/ deletes objects that
176
+ // canvases in git history still point at, with no recovery path.
177
+ const ids = verificationPlan(ok({ ...BASE, s3: S3 })).map((s) => s.id);
178
+ assert.ok(ids.includes('s3-object'));
179
+ assert.ok(ids.includes('s3-no-expiry'));
180
+ assert.ok(!verificationPlan(ok(BASE)).some((s) => s.id === 's3-no-expiry'));
181
+ });
182
+
183
+ test('every step states what it checks, in words an operator can act on', () => {
184
+ for (const step of verificationPlan(ok({ ...BASE, s3: S3 }))) {
185
+ assert.ok(step.title.length > 5, `step ${step.id} needs a title`);
186
+ assert.ok(step.detail.length > 10, `step ${step.id} needs a detail`);
187
+ }
188
+ });
189
+
190
+ // -------------------------------------------------------- operator duties
191
+
192
+ test('the run never claims to own the deployment forever', () => {
193
+ // The breaker trap: "done" implies ownership. This scaffolded and verified it
194
+ // once; rotation, backups, upgrades and the bill stay with the operator.
195
+ const duties = operatorDuties(ok(BASE));
196
+ const text = duties
197
+ .map((d) => `${d.title} ${d.detail}`)
198
+ .join(' ')
199
+ .toLowerCase();
200
+ assert.ok(duties.length >= 5);
201
+ assert.match(text, /rotate/);
202
+ assert.match(text, /restore drill/);
203
+ assert.match(text, /upgrade/);
204
+ assert.match(text, /bill/);
205
+ });
206
+
207
+ test('the duties card warns about `latest` by name', () => {
208
+ const pinned = operatorDuties(ok({ ...BASE, imageTag: 'v0.48.0' }));
209
+ assert.match(pinned.find((d) => /pin/i.test(d.title)).detail, /v0\.48\.0/);
210
+ const floating = operatorDuties(ok(BASE));
211
+ assert.match(floating.find((d) => /pin/i.test(d.title)).detail, /unplanned upgrade/);
212
+ });
213
+
214
+ test('object storage adds the never-expire duty; dev MinIO adds its own warning', () => {
215
+ const withS3 = operatorDuties(ok({ ...BASE, s3: S3 }))
216
+ .map((d) => d.title)
217
+ .join(' ');
218
+ assert.match(withS3, /Never expire the assets\/ prefix/);
219
+ const dev = operatorDuties(ok({ ...BASE, devMinio: true }))
220
+ .map((d) => d.title)
221
+ .join(' ');
222
+ assert.match(dev, /MinIO credentials are in this directory/);
223
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1agh/maude",
3
- "version": "0.47.0",
3
+ "version": "0.49.0",
4
4
  "description": "Marketplace of Claude Code plugins by Michal Dovrtěl: `design` (canvas-first design iteration) + `flow` (generic agentic workflow loop with .ai second brain). Ships the `maude` CLI (with `mdcc` legacy alias) to scaffold workspace, run the design dev server, and manage configs.",
5
5
  "type": "module",
6
6
  "engines": {
@@ -51,13 +51,13 @@
51
51
  "test:e2e:desktop:acp-cold-start": "pnpm --filter @maude/desktop-e2e e2e:acp-cold-start"
52
52
  },
53
53
  "optionalDependencies": {
54
- "@1agh/maude-darwin-arm64": "0.47.0",
55
- "@1agh/maude-darwin-x64": "0.47.0",
56
- "@1agh/maude-linux-arm64": "0.47.0",
57
- "@1agh/maude-linux-arm64-musl": "0.47.0",
58
- "@1agh/maude-linux-x64": "0.47.0",
59
- "@1agh/maude-linux-x64-musl": "0.47.0",
60
- "@1agh/maude-win32-x64": "0.47.0"
54
+ "@1agh/maude-darwin-arm64": "0.49.0",
55
+ "@1agh/maude-darwin-x64": "0.49.0",
56
+ "@1agh/maude-linux-arm64": "0.49.0",
57
+ "@1agh/maude-linux-arm64-musl": "0.49.0",
58
+ "@1agh/maude-linux-x64": "0.49.0",
59
+ "@1agh/maude-linux-x64-musl": "0.49.0",
60
+ "@1agh/maude-win32-x64": "0.49.0"
61
61
  },
62
62
  "files": [
63
63
  "cli",
@@ -146,6 +146,23 @@
146
146
  "usedBy": ["commands/init.md", "skills/design/SKILL.md"],
147
147
  "docsUrl": "https://github.com/1aGh/maude"
148
148
  },
149
+ {
150
+ "id": "kg",
151
+ "type": "cli",
152
+ "hardness": "soft",
153
+ "check": {
154
+ "command": "kg version",
155
+ "expectExit": 0
156
+ },
157
+ "install": {
158
+ "preferred": "See docs/kgai-onboarding.md — download the pinned kgai release binary (kgai is capability-gated + opt-out; nothing to install to use classic .ai/ mode)",
159
+ "darwin": "curl -fsSL https://github.com/kgaidev/kgai/releases/download/v0.1.9/kg-darwin-arm64 -o /usr/local/bin/kg && chmod +x /usr/local/bin/kg"
160
+ },
161
+ "autoInstall": false,
162
+ "fallbackBehavior": "Design DS/canvas/brand decisions are not recorded to the kgai graph; the classic .ai/ path (via /flow:record-ddr) and on-disk sidecars are unchanged.",
163
+ "usedBy": ["skills/design-system/SKILL.md", "commands/edit.md"],
164
+ "docsUrl": "https://github.com/kgaidev/kgai"
165
+ },
149
166
  {
150
167
  "id": "jq",
151
168
  "type": "system-tool",
@@ -305,6 +305,72 @@
305
305
  }
306
306
  }
307
307
  },
308
+ "knowledgeGraph": {
309
+ "type": "object",
310
+ "additionalProperties": false,
311
+ "description": "Opt-OUT, capability-gated knowledge-graph memory backend (kgai). ON by default when the capability exists: an absent `knowledgeGraph` block is treated as `mode:auto`, so when the `kg` CLI is on PATH AND a store is resolvable, flow/design decision reads/writes route through the event-sourced graph instead of `.ai/decisions/` + `.ai/state/STATE.md`. When `kg` is absent, the store is unreachable, or `mode:off`, everything falls back to today's `.ai/` file behavior — no regression, nothing to configure downstream. Mirrors the `orchestration` capability-gate (DDR-130). Design plugin reads the SAME block. See the `kgai-backend` skill for the resolver.",
312
+ "properties": {
313
+ "mode": {
314
+ "type": "string",
315
+ "enum": ["auto", "on", "off"],
316
+ "default": "auto",
317
+ "description": "Opt-out default `auto` (absent block == `auto`). `auto` = active only when `kg` is on PATH AND (`store` is set OR a local `.kgai/store` exists) — conservative, mirrors `orchestration.mode:auto`; a first-run repo stays on files until the user opts in via onboarding. `on` = force kgai active (errors surface rather than silently falling back). `off` = disable entirely; today's classic `.ai/decisions/` + STATE.md path, byte-for-byte unchanged."
318
+ },
319
+ "engine": {
320
+ "type": "string",
321
+ "enum": ["kgai"],
322
+ "default": "kgai",
323
+ "description": "Knowledge-graph engine. Reserved for future engines; only `kgai` is implemented."
324
+ },
325
+ "engineVersion": {
326
+ "type": "string",
327
+ "default": "v0.1.9",
328
+ "description": "PINNED kgai release the CLI + desktop bundle target. Advance deliberately (harness-verified via `maude kg check-upstream`), never float — auto-tracking `latest` would let an upstream change silently alter behavior mid-rollout and is a supply-chain surface (DDR-054/056 reasoning: the bundled `kg` is third-party)."
329
+ },
330
+ "store": {
331
+ "type": "string",
332
+ "default": "",
333
+ "description": "Remote store the graph syncs to. `s3://company-kg/store` (shared company remote, cross-repo) | `kgai://org/project` (cloud-broker keyspace) | git remote | `\"\"` = local-only `.kgai/store`. Empty string keeps everything on this machine."
334
+ },
335
+ "scope": {
336
+ "type": "object",
337
+ "additionalProperties": false,
338
+ "description": "Cross-repo scope stamped on every write, so one shared store can host many repos/departments and search can bias to the local scope first. Project-agnostic — values are per-repo config, never hardcoded in the plugin.",
339
+ "properties": {
340
+ "repo": {
341
+ "type": "string",
342
+ "description": "This repo's name, stamped as a `repo:<name>` element + `IN_REPO` link on every decision."
343
+ },
344
+ "dept": {
345
+ "type": "string",
346
+ "description": "This repo's department (e.g. dev/marketing/finance/automations), stamped as a `dept:<name>` element + `IN_DEPT` link. Default search bias — reads re-rank `dept:<this>` hits first; `--all-scopes` widens."
347
+ }
348
+ }
349
+ },
350
+ "capture": {
351
+ "type": "object",
352
+ "additionalProperties": false,
353
+ "description": "Which write paths route into the graph when active. All default `true`.",
354
+ "properties": {
355
+ "decisions": {
356
+ "type": "boolean",
357
+ "default": true,
358
+ "description": "Route `/flow:record-ddr` + DDR-worthy design writes to `kg ingest` instead of a `DDR-NNN.md` file."
359
+ },
360
+ "state": {
361
+ "type": "boolean",
362
+ "default": true,
363
+ "description": "Route STATE.md history / pause-resume events to kgai as dated events."
364
+ },
365
+ "auto": {
366
+ "type": "boolean",
367
+ "default": true,
368
+ "description": "Let kgai's own `Stop` hook nudge autonomous decision capture after structural edits (zero added LLM cost). Set `false` to suppress the nudge."
369
+ }
370
+ }
371
+ }
372
+ }
373
+ },
308
374
  "security": {
309
375
  "type": "object",
310
376
  "additionalProperties": false,
@@ -95,6 +95,23 @@
95
95
  ],
96
96
  "docsUrl": "https://github.com/anthropics/agent-device"
97
97
  },
98
+ {
99
+ "id": "kg",
100
+ "type": "cli",
101
+ "hardness": "soft",
102
+ "check": {
103
+ "command": "kg version",
104
+ "expectExit": 0
105
+ },
106
+ "install": {
107
+ "preferred": "See docs/kgai-onboarding.md — download the pinned kgai release binary (kgai is capability-gated + opt-out; nothing to install to use classic .ai/ mode)",
108
+ "darwin": "curl -fsSL https://github.com/kgaidev/kgai/releases/download/v0.1.9/kg-darwin-arm64 -o /usr/local/bin/kg && chmod +x /usr/local/bin/kg"
109
+ },
110
+ "autoInstall": false,
111
+ "fallbackBehavior": "flow/design fall back to the classic .ai/decisions/ + STATE.md file path (no regression). The kgai knowledge-graph backend (kg context/ingest/sync, cross-repo company memory) is inactive.",
112
+ "usedBy": ["skills/kgai-backend/SKILL.md", "commands/migrate-kgai.md"],
113
+ "docsUrl": "https://github.com/kgaidev/kgai"
114
+ },
98
115
  {
99
116
  "id": "jq",
100
117
  "type": "system-tool",