@passioncode-ai/passioncode 0.1.38 → 0.1.40

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 (27) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/family.json +1 -1
  3. package/package.json +1 -1
  4. package/payload/.claude-plugin/marketplace.json +2 -2
  5. package/payload/manifest.json +8 -8
  6. package/payload/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
  7. package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +1 -1
  8. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +3 -2
  9. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/demo.html +1 -0
  10. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-button.js +280 -0
  11. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.css +30 -1
  12. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.js +2 -2
  13. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/kit.json +9 -3
  14. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-kit.md +31 -2
  15. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/profiles.md +64 -0
  16. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +1 -1
  17. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +11 -0
  18. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/dashboard_kit.py +7 -4
  19. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-profiles.mjs +196 -0
  20. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-secrets.mjs +86 -0
  21. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +41 -7
  22. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_profiles.py +277 -0
  23. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_secrets.py +132 -0
  24. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +54 -9
  25. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +4 -2
  26. package/payload/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +1 -1
  27. package/payload/plugins/passioncode/.claude-plugin/plugin.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.40 - 2026-10-12
4
+
5
+ ### Changed
6
+
7
+ - **Fabric Agent Adapter is pinned at `v0.16.0`** (it was `v0.15.0`). Runner and model profiles in both kits (contract DEC-0046). The kit resolves the model profile in the contract's order within the project's model classes and declares what applies in the well-known configuration, with Runner and Model tiles. It checks the runner and model in the doctor and records the route on usage. The contract's credential detector is ported.
8
+
9
+ ## 0.1.39 - 2026-10-12
10
+
11
+ ### Changed
12
+
13
+ - **Fabric Agent Adapter is pinned at `v0.15.0`** (it was `v0.14.0`). The dashboard kit ships Fabric's button (contract DEC-0040, FB-7). It is one floating menu button that opens Fabric on the service, with links built from the contract's grammar. It hides itself when the host draws its own and says how to get Fabric when Fabric is absent. Dashboard kit 1.1.0.
14
+
3
15
  ## 0.1.38 - 2026-10-12
4
16
 
5
17
  ### Changed
package/family.json CHANGED
@@ -8,7 +8,7 @@
8
8
  "name": "fabric-agent-adapter",
9
9
  "displayName": "Fabric Agent Adapter",
10
10
  "repo": "passioncode-ai/fabric-agent-adapter",
11
- "ref": "v0.14.0",
11
+ "ref": "v0.16.0",
12
12
  "kind": "plugin",
13
13
  "path": "plugins/fabric-agent-adapter",
14
14
  "legacyPluginIds": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@passioncode-ai/passioncode",
3
- "version": "0.1.38",
3
+ "version": "0.1.40",
4
4
  "description": "The PassionCode.ai agent skill set \u2014 Fabric Agent Adapter, Observatory Log and the organisation's working rules \u2014 for Claude Code and every other agent on the machine, updated as one set.",
5
5
  "bin": {
6
6
  "passioncode": "bin/passioncode.js"
@@ -11,7 +11,7 @@
11
11
  "name": "fabric-agent-adapter",
12
12
  "displayName": "Fabric Agent Adapter",
13
13
  "source": "./plugins/fabric-agent-adapter",
14
- "version": "0.14.0",
14
+ "version": "0.16.0",
15
15
  "description": "Create new Fabric-compatible agents, build them as always-alive local services with dashboards (fabric-service/0.1), and adapt existing projects to the Fabric Agent Contract through evidence-backed MCP, A2A, or local-runner provider bundles.",
16
16
  "author": {
17
17
  "name": "PassionCode.ai",
@@ -35,7 +35,7 @@
35
35
  "name": "passioncode",
36
36
  "displayName": "PassionCode.ai",
37
37
  "source": "./plugins/passioncode",
38
- "version": "0.1.38",
38
+ "version": "0.1.40",
39
39
  "description": "The organisation's working rules for every contributor's agent (working-in-passioncode), and a once-a-day check that keeps the PassionCode.ai set current.",
40
40
  "author": {
41
41
  "name": "PassionCode.ai",
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "family": "passioncode",
3
- "version": "0.1.38",
3
+ "version": "0.1.40",
4
4
  "release": true,
5
5
  "members": [
6
6
  {
7
7
  "name": "fabric-agent-adapter",
8
8
  "displayName": "Fabric Agent Adapter",
9
9
  "repo": "passioncode-ai/fabric-agent-adapter",
10
- "ref": "v0.14.0",
11
- "commit": "d492e820ba922cc5eaa109b1aaae6dce12319569",
12
- "version": "0.14.0",
10
+ "ref": "v0.16.0",
11
+ "commit": "a7ab9aa5cc494cc3ce259b1fb128f97dc9e76545",
12
+ "version": "0.16.0",
13
13
  "via": "clone of https://***@github.com/passioncode-ai/fabric-agent-adapter.git",
14
14
  "skills": [
15
15
  "adapting-projects-to-fabric",
@@ -22,7 +22,7 @@
22
22
  "legacyMarketplaces": [
23
23
  "fabric-agent-adapter"
24
24
  ],
25
- "contentHash": "sha256:37413cb1e4e8f38267eead524e592923270029396d72bb0958301c0e549d933c",
25
+ "contentHash": "sha256:ebdae2bfa61d5a6c70f8b8f2eb2ad96d0c2e09adfd912b527830a7fcdd4a48a4",
26
26
  "description": "Create new Fabric-compatible agents, build them as always-alive local services with dashboards (fabric-service/0.1), and adapt existing projects to the Fabric Agent Contract through evidence-backed MCP, A2A, or local-runner provider bundles.",
27
27
  "author": {
28
28
  "name": "PassionCode.ai",
@@ -61,16 +61,16 @@
61
61
  "name": "passioncode",
62
62
  "displayName": "PassionCode.ai",
63
63
  "repo": "passioncode-ai/passioncode",
64
- "ref": "v0.1.38",
64
+ "ref": "v0.1.40",
65
65
  "commit": null,
66
- "version": "0.1.38",
66
+ "version": "0.1.40",
67
67
  "via": "this repository",
68
68
  "skills": [
69
69
  "working-in-passioncode"
70
70
  ],
71
71
  "legacyPluginIds": [],
72
72
  "legacyMarketplaces": [],
73
- "contentHash": "sha256:b006d8d8ff568b7e5c90255859aa4ea7c6911236e185e9848a65b722054922e2",
73
+ "contentHash": "sha256:0d35360242483ba3cee04c94d249d2ab4a46963a9ddc38af8075fac936655f0c",
74
74
  "description": "The organisation's working rules for every contributor's agent (working-in-passioncode), and a once-a-day check that keeps the PassionCode.ai set current.",
75
75
  "author": {
76
76
  "name": "PassionCode.ai",
@@ -3,7 +3,7 @@
3
3
  "name": "fabric-agent-adapter",
4
4
  "displayName": "Fabric Agent Adapter",
5
5
  "description": "Create new Fabric-compatible agents, build them as always-alive local services with dashboards (fabric-service/0.1), and adapt existing projects to the Fabric Agent Contract through evidence-backed MCP, A2A, or local-runner provider bundles.",
6
- "version": "0.14.0",
6
+ "version": "0.16.0",
7
7
  "author": {
8
8
  "name": "PassionCode.ai",
9
9
  "url": "https://passioncode.ai/"
@@ -5,7 +5,7 @@ license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
5
5
  compatibility: Requires filesystem access and Python 3.9+. Exact schema checks additionally need git, Node.js, pnpm, and the pinned fabric-agent-contract checkout (a public repository). Works without those tools in an explicitly degraded structural-check mode.
6
6
  metadata:
7
7
  author: PassionCode.ai
8
- version: "0.14.0"
8
+ version: "0.16.0"
9
9
  contract-version: "0.1.0"
10
10
  contract-commit: "0cacd08b0e95b6abf131eae1206b60792138bc3c"
11
11
  ---
@@ -16,7 +16,7 @@ license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
16
16
  compatibility: Python 3.9+ or Node.js 20+ for the kits; the probe needs Python 3.9+. launchd steps are macOS-only (Linux services use lifecycle manager none until a systemd adapter exists). Dashboard handoff optionally uses Fabric Dashboards MCP link/host_status/open; without it, report unresolved host capability. The contract checkout is optional.
17
17
  metadata:
18
18
  author: PassionCode.ai
19
- version: "0.14.0"
19
+ version: "0.16.0"
20
20
  contract-version: "0.1.0"
21
21
  extension: "fabric-service/0.1"
22
22
  extension-commit: "0cacd08b0e95b6abf131eae1206b60792138bc3c"
@@ -83,7 +83,8 @@ are in [`scripts/fabric_interop.py`](scripts/fabric_interop.py) and
83
83
  [`scripts/fabric-interop.mjs`](scripts/fabric-interop.mjs). When the callers are agents of the estate, the
84
84
  service checks each call's delegation token (DEC-0039): read [the delegation reference](references/delegation.md);
85
85
  the verifier is [`scripts/fabric_delegation.py`](scripts/fabric_delegation.py) /
86
- [`scripts/fabric-delegation.mjs`](scripts/fabric-delegation.mjs).
86
+ [`scripts/fabric-delegation.mjs`](scripts/fabric-delegation.mjs). When Fabric chooses the service's runner and models
87
+ (DEC-0046), read [the profiles reference](references/profiles.md).
87
88
 
88
89
  ## Step 2 — the non-negotiables
89
90
 
@@ -7,6 +7,7 @@
7
7
  <title>Example agent · dashboard kit demo</title>
8
8
  <link rel="stylesheet" href="fabric-dashboard.css">
9
9
  <script src="fabric-dashboard.js" defer></script>
10
+ <script src="fabric-button.js" defer data-service="example-agent.default" data-service-name="Example Agent" data-project="project-a" data-project-name="Project A"></script>
10
11
  </head>
11
12
  <body>
12
13
  <a class="fd-skip" href="#overview">Skip to content</a>
@@ -0,0 +1,280 @@
1
+ /* fabric-dashboard-kit@1.1.0 · Fabric's button (Fabric Agent Contract DEC-0040, FB-1…FB-8, FL-1…FL-6)
2
+ Load it same-origin, after fabric-dashboard.css, with the service it opens Fabric on:
3
+ <script src="fabric-button.js" defer data-service="example-agent.default" data-service-name="Example Agent"
4
+ data-project="project-a" data-project-name="Project A"></script>
5
+ One floating menu button: "Chat with Fabric about <service>" and, when the page knows its project, "Open a coding
6
+ session in <project>". Its links carry ids only and open a view in Fabric, nothing more (FB-8). It hides itself
7
+ when the host draws its own (the fabric_button cookie, FB-6) and says how to get Fabric when Fabric is absent
8
+ (FB-5). No inline script, no eval, nothing from a CDN; the only address it names is passioncode.ai/fabric.
9
+ Do not edit in place; `dashboard_kit.py install` replaces it on an upgrade. */
10
+ (function (root) {
11
+ 'use strict';
12
+
13
+ var VERSION = '1.1.0';
14
+ var COOKIE = 'fabric_button';
15
+ var GET_FABRIC = 'https://passioncode.ai/fabric';
16
+ var ABSENT_AFTER_MS = 1500;
17
+ var MAX_LINK_LENGTH = 2048;
18
+
19
+ // ---- fabric:// links: a port of the contract's src/fabric-links.ts and src/secret-shape.ts (FL-1…FL-6) ----
20
+
21
+ var AGENT_ID = /^[a-z][a-z0-9-]{1,62}(\.[a-z][a-z0-9-]{0,31})?$/;
22
+ var PROJECT_REF = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/;
23
+ var RUNNER_KIND = /^[a-z][a-z0-9-]{1,62}$/;
24
+ var SHAPE = /^fabric:\/\/(chat|session\/new)\/?(?:\?([^#]*))?(?:#(.*))?$/;
25
+ var KNOWN = {
26
+ chat: { service: AGENT_ID, project: PROJECT_REF },
27
+ 'session.new': { project: PROJECT_REF, runner: RUNNER_KIND }
28
+ };
29
+ var PROMPT_WORDS = ['prompt', 'message', 'msg', 'text', 'instruction', 'instructions'];
30
+ var CREDENTIAL_WORDS = ['token', 'secret', 'password', 'passwd', 'pass', 'passphrase', 'credential', 'credentials', 'cookie',
31
+ 'authorization', 'bearer', 'jwt', 'otp', 'apikey', 'signature', 'sig', 'accesstoken', 'idtoken', 'authtoken',
32
+ 'refreshtoken', 'sessionid', 'sessiontoken', 'logincode', 'authcode', 'privatekey', 'clientsecret', 'secretkey', 'accesskey'];
33
+ var CREDENTIAL_PAIRS = {
34
+ key: ['private', 'api', 'access', 'secret', 'session', 'auth', 'signing', 'encryption', 'master', 'client', 'login', 'refresh'],
35
+ auth: ['token', 'code', 'header'],
36
+ session: ['token', 'id'],
37
+ code: ['login', 'auth', 'access', 'session']
38
+ };
39
+ var JWT_SHAPE = /eyJ[A-Za-z0-9_-]{2,}\.[A-Za-z0-9_-]{2,}\.[A-Za-z0-9_-]*/;
40
+ var SECRET_VALUE = /(^bearer\s.+|sk-[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9]{20,}|xox[abposr]-[A-Za-z0-9-]{10,}|AKIA[0-9A-Z]{16}|glpat-[A-Za-z0-9_-]{20,})/i;
41
+ var LONG_RUN = /[A-Za-z0-9+/=_-]{40,}/g;
42
+
43
+ function nameWords(name) {
44
+ return name.replace(/([a-z0-9])([A-Z])/g, '$1 $2').replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
45
+ .split(/[\s._-]+/).filter(Boolean).map(function (w) { return w.toLowerCase(); });
46
+ }
47
+ function withSingulars(words) {
48
+ return words.concat(words.filter(function (w) { return w.length > 2 && /s$/.test(w); }).map(function (w) { return w.slice(0, -1); }));
49
+ }
50
+ function credentialName(name) {
51
+ var split = nameWords(name);
52
+ var words = withSingulars(split);
53
+ if (words.some(function (w) { return CREDENTIAL_WORDS.indexOf(w) >= 0; })) return true;
54
+ if (split.length === 1 && words.some(function (w) { return Object.prototype.hasOwnProperty.call(CREDENTIAL_PAIRS, w); })) return true;
55
+ return Object.keys(CREDENTIAL_PAIRS).some(function (w) {
56
+ return words.indexOf(w) >= 0 && CREDENTIAL_PAIRS[w].some(function (p) { return words.indexOf(p) >= 0; });
57
+ });
58
+ }
59
+ function longSecret(value) {
60
+ return (value.match(LONG_RUN) || []).some(function (run) { return /[a-z]/.test(run) && /[A-Z]/.test(run) && /[0-9]/.test(run); });
61
+ }
62
+ function secretValue(value) { return JWT_SHAPE.test(value) || SECRET_VALUE.test(value) || longSecret(value); }
63
+ function decodeKeepingPlus(raw) { try { return decodeURIComponent(raw); } catch (e) { return raw; } }
64
+ function firstKey(text) { var it = new URLSearchParams(text).keys().next(); return it.done ? '' : it.value; }
65
+ function rawValues(query) {
66
+ var values = new Map();
67
+ query.split('&').forEach(function (piece) {
68
+ if (piece === '') return;
69
+ var at = piece.indexOf('=');
70
+ var name = firstKey(at < 0 ? piece : piece.slice(0, at));
71
+ values.set(name, (values.get(name) || []).concat(at < 0 ? '' : decodeKeepingPlus(piece.slice(at + 1))));
72
+ });
73
+ return values;
74
+ }
75
+ function promptName(name) { return nameWords(name).some(function (w) { return PROMPT_WORDS.indexOf(w) >= 0; }); }
76
+ function flat(lists) { return lists.reduce(function (a, b) { return a.concat(b); }, []); }
77
+
78
+ /** The contract's parseFabricLink: {ok: true, link, ignored} or {ok: false, problems}. */
79
+ function parseFabricLink(url) {
80
+ if (url.length > MAX_LINK_LENGTH) return { ok: false, problems: ['a link is at most ' + MAX_LINK_LENGTH + ' characters'] };
81
+ var match = SHAPE.exec(url.replace(/^fabric:/i, 'fabric:'));
82
+ if (!match) return { ok: false, problems: ['not fabric://chat or fabric://session/new'] };
83
+ var action = match[1] === 'chat' ? 'chat' : 'session.new';
84
+ var params = new URLSearchParams(match[2] || '');
85
+ var problems = [];
86
+ var fragment = match[3] || '';
87
+ if (fragment) {
88
+ var pieces = Array.from(new URLSearchParams(fragment));
89
+ var raw = flat(Array.from(rawValues(fragment).values()));
90
+ if (secretValue(fragment) || raw.some(secretValue) || pieces.some(function (p) { return (p[1] !== '' && credentialName(p[0])) || secretValue(p[1]) || secretValue(p[0]); })) {
91
+ problems.push('the fragment could carry a credential; a link never does');
92
+ }
93
+ }
94
+ var ignored = [];
95
+ var values = {};
96
+ var known = KNOWN[action];
97
+ var rawQuery = rawValues(match[2] || '');
98
+ Array.from(new Set(Array.from(params.keys()))).forEach(function (name) {
99
+ var all = params.getAll(name);
100
+ if (promptName(name)) { problems.push('the parameter ' + name + ' carries words to act on; a link carries ids only'); return; }
101
+ if (credentialName(name) || secretValue(name) || all.some(secretValue) || (rawQuery.get(name) || []).some(secretValue)) {
102
+ problems.push('the parameter ' + name + ' could carry a credential; a link never does'); return;
103
+ }
104
+ var grammar = Object.prototype.hasOwnProperty.call(known, name) ? known[name] : undefined;
105
+ if (!grammar) { ignored.push(name); return; }
106
+ if (all.length > 1) { problems.push(name + ' is given ' + all.length + ' times'); return; }
107
+ var value = all[0] || '';
108
+ if (!grammar.test(value)) {
109
+ problems.push(name + ' is not a valid ' + (name === 'service' ? 'agent id' : name === 'runner' ? 'runner kind' : 'project id or slug')); return;
110
+ }
111
+ values[name] = value;
112
+ });
113
+ var named = function (re) { return problems.some(function (p) { return re.test(p); }); };
114
+ if (action === 'chat' && values.service === undefined && values.project === undefined && !named(/^(service|project) /)) problems.push('a chat link names a service, a project or both');
115
+ if (action === 'session.new' && values.project === undefined && !named(/^project /)) problems.push('a session link names its project');
116
+ if (problems.length) return { ok: false, problems: problems };
117
+ var link = action === 'chat' ? { action: action } : { action: action, project: values.project };
118
+ if (action === 'chat') { if (values.service) link.service = values.service; if (values.project) link.project = values.project; }
119
+ else if (values.runner) link.runner = values.runner;
120
+ return { ok: true, link: link, ignored: ignored };
121
+ }
122
+
123
+ /** The contract's buildFabricLink: throws on a value the grammar refuses, so a page cannot emit a bad link. */
124
+ function buildFabricLink(link) {
125
+ var query = new URLSearchParams();
126
+ if (link.action === 'chat') {
127
+ if (link.service !== undefined) query.set('service', link.service);
128
+ if (link.project !== undefined) query.set('project', link.project);
129
+ } else {
130
+ query.set('project', link.project);
131
+ if (link.runner !== undefined) query.set('runner', link.runner);
132
+ }
133
+ var url = 'fabric://' + (link.action === 'chat' ? 'chat' : 'session/new') + '?' + query.toString();
134
+ var parsed = parseFabricLink(url);
135
+ if (!parsed.ok) throw new TypeError(parsed.problems.join('; '));
136
+ return url;
137
+ }
138
+
139
+ /** FB-6: the host's cookie as {owner, fabric}, or null when absent or of an unknown version. */
140
+ function parseButtonCookie(cookieHeader) {
141
+ if (typeof cookieHeader !== 'string' || !cookieHeader) return null;
142
+ var parts = cookieHeader.split(';');
143
+ for (var i = 0; i < parts.length; i++) {
144
+ var part = parts[i].replace(/^\s+/, '');
145
+ if (part.indexOf(COOKIE + '=') !== 0) continue;
146
+ var raw = part.slice(COOKIE.length + 1);
147
+ if (raw.charAt(0) === '"' && raw.charAt(raw.length - 1) === '"') raw = raw.slice(1, -1);
148
+ var fields = new URLSearchParams(decodeKeepingPlus(raw));
149
+ if (fields.get('v') !== '1') return null;
150
+ var out = {};
151
+ if (fields.get('owner') === 'host' || fields.get('owner') === 'page') out.owner = fields.get('owner');
152
+ if (fields.get('fabric') === 'present' || fields.get('fabric') === 'absent') out.fabric = fields.get('fabric');
153
+ return out;
154
+ }
155
+ return null;
156
+ }
157
+
158
+ /** The menu items for a page: the chat link always, the session link when the page knows its project. */
159
+ function menuItems(options) {
160
+ var items = [];
161
+ var chat = { action: 'chat', service: options.service };
162
+ if (options.project) chat.project = options.project;
163
+ items.push({ label: 'Chat with Fabric about ' + (options.serviceName || options.service), href: buildFabricLink(chat) });
164
+ if (options.project) {
165
+ var session = { action: 'session.new', project: options.project };
166
+ if (options.runner) session.runner = options.runner;
167
+ items.push({ label: 'Open a coding session in ' + (options.projectName || options.project), href: buildFabricLink(session) });
168
+ }
169
+ return items;
170
+ }
171
+
172
+ // ---- the button (FB-1…FB-6) ----
173
+
174
+ var SVG = 'http://www.w3.org/2000/svg';
175
+
176
+ function mark(doc) {
177
+ var svg = doc.createElementNS(SVG, 'svg');
178
+ svg.setAttribute('viewBox', '0 0 24 24');
179
+ svg.setAttribute('aria-hidden', 'true');
180
+ svg.setAttribute('focusable', 'false');
181
+ svg.setAttribute('class', 'fd-fabric-button-mark');
182
+ var path = doc.createElementNS(SVG, 'path');
183
+ path.setAttribute('d', 'M5 5h14v3H8v3h9v3H8v5H5z'); // the Fabric mark: an F
184
+ path.setAttribute('fill', 'currentColor');
185
+ svg.appendChild(path);
186
+ return svg;
187
+ }
188
+
189
+ function el(doc, tag, attrs, text) {
190
+ var node = doc.createElement(tag);
191
+ Object.keys(attrs || {}).forEach(function (k) { node.setAttribute(k, attrs[k]); });
192
+ if (text !== undefined) node.textContent = text;
193
+ return node;
194
+ }
195
+
196
+ /** Mount the button in `doc`. Returns the button, or null when the host draws its own (FB-1, FB-6). */
197
+ function mount(doc, options) {
198
+ var cookie = parseButtonCookie(doc.cookie);
199
+ if (cookie && cookie.owner === 'host') return null;
200
+ var items = menuItems(options);
201
+ var absent = options.fabric === 'absent' || (cookie && cookie.fabric === 'absent');
202
+
203
+ var wrap = el(doc, 'div', { 'class': 'fd-fabric-button-wrap' });
204
+ var button = el(doc, 'button', { type: 'button', 'class': 'fd-fabric-button', 'aria-haspopup': 'menu', 'aria-expanded': 'false', 'aria-label': 'Open Fabric' });
205
+ button.appendChild(mark(doc));
206
+ button.appendChild(el(doc, 'span', { 'class': 'fd-fabric-button-label', 'aria-hidden': 'true' }, 'Fabric'));
207
+ var menu = el(doc, 'div', { role: 'menu', 'class': 'fd-fabric-button-menu', 'aria-label': 'Fabric' });
208
+ menu.hidden = true;
209
+ var links = items.map(function (item) {
210
+ var a = el(doc, 'a', { role: 'menuitem', href: item.href, tabindex: '-1', 'class': 'fd-fabric-button-item' }, item.label);
211
+ menu.appendChild(a);
212
+ return a;
213
+ });
214
+ var note = el(doc, 'p', { 'class': 'fd-fabric-button-note', role: 'note' });
215
+ note.appendChild(doc.createTextNode('Fabric is not installed — get it at '));
216
+ note.appendChild(el(doc, 'a', { href: GET_FABRIC, target: '_blank', rel: 'noopener', 'class': 'fd-fabric-button-get' }, 'passioncode.ai/fabric'));
217
+ note.hidden = !absent;
218
+ menu.appendChild(note);
219
+ wrap.appendChild(menu);
220
+ wrap.appendChild(button);
221
+ doc.body.appendChild(wrap);
222
+ doc.documentElement.classList.add('fd-has-fabric-button');
223
+
224
+ function open(focusFirst) {
225
+ menu.hidden = false;
226
+ button.setAttribute('aria-expanded', 'true');
227
+ if (focusFirst && links.length) links[0].focus();
228
+ }
229
+ function close(refocus) {
230
+ menu.hidden = true;
231
+ button.setAttribute('aria-expanded', 'false');
232
+ if (refocus) button.focus();
233
+ }
234
+ function move(by) {
235
+ var at = links.indexOf(doc.activeElement);
236
+ links[(at + by + links.length) % links.length].focus();
237
+ }
238
+ button.addEventListener('click', function () { if (menu.hidden) open(true); else close(true); });
239
+ button.addEventListener('keydown', function (e) {
240
+ if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { e.preventDefault(); open(false); (e.key === 'ArrowDown' ? links[0] : links[links.length - 1]).focus(); }
241
+ });
242
+ menu.addEventListener('keydown', function (e) {
243
+ if (e.key === 'Escape') { e.preventDefault(); close(true); }
244
+ else if (e.key === 'ArrowDown') { e.preventDefault(); move(1); }
245
+ else if (e.key === 'ArrowUp') { e.preventDefault(); move(-1); }
246
+ else if (e.key === 'Home') { e.preventDefault(); links[0].focus(); }
247
+ else if (e.key === 'End') { e.preventDefault(); links[links.length - 1].focus(); }
248
+ else if (e.key === 'Tab') { close(false); }
249
+ });
250
+ doc.addEventListener('click', function (e) { if (!menu.hidden && !wrap.contains(e.target)) close(false); });
251
+ // FB-5: a page cannot see which schemes this computer handles. If, after following a link, the page is still
252
+ // visible and focused 1.5 s later, Fabric did not take it: say how to get Fabric, once, and never retry.
253
+ links.forEach(function (a) {
254
+ a.addEventListener('click', function () {
255
+ root.setTimeout(function () {
256
+ if (doc.visibilityState === 'visible' && doc.hasFocus()) { note.hidden = false; open(false); }
257
+ }, ABSENT_AFTER_MS);
258
+ });
259
+ });
260
+ return button;
261
+ }
262
+
263
+ function optionsFrom(script) {
264
+ var d = script.dataset;
265
+ return { service: d.service, serviceName: d.serviceName, project: d.project, projectName: d.projectName, runner: d.runner, fabric: d.fabric };
266
+ }
267
+
268
+ var api = { version: VERSION, parseFabricLink: parseFabricLink, buildFabricLink: buildFabricLink, parseButtonCookie: parseButtonCookie,
269
+ menuItems: menuItems, credentialName: credentialName, secretValue: secretValue, nameWords: nameWords, mount: mount };
270
+ root.FabricButton = api;
271
+
272
+ var doc = root.document;
273
+ var script = doc && doc.currentScript;
274
+ if (script && script.dataset && script.dataset.service) {
275
+ var start = function () {
276
+ try { mount(doc, optionsFrom(script)); } catch (err) { if (root.console) root.console.warn('fabric-button: not shown — ' + err.message); }
277
+ };
278
+ if (doc.readyState === 'loading') doc.addEventListener('DOMContentLoaded', start); else start();
279
+ }
280
+ })(typeof window !== 'undefined' ? window : globalThis);
@@ -1,4 +1,4 @@
1
- /* fabric-dashboard-kit@1.0.0 · fabric-dashboard/0.1 (Fabric Agent Contract DEC-0036)
1
+ /* fabric-dashboard-kit@1.1.0 · fabric-dashboard/0.1 (Fabric Agent Contract DEC-0036)
2
2
  A Fabric service dashboard shown inside a host pane: usable from 360 CSS px up, navigation always
3
3
  reachable, wide content in its own scroll box, one vertical scroller, appearance from the host.
4
4
  Copy this file next to fabric-dashboard.js; do not edit it in place. Put the page's own rules in
@@ -196,3 +196,32 @@ pre.fd-scroll, .fd-scroll pre { margin: 0; padding: var(--space-3); font: var(--
196
196
 
197
197
  /* ---- appearance owned by the host (ED-6) ---- */
198
198
  html[data-fd-host-appearance] [data-fd-appearance-control] { display: none !important; }
199
+
200
+ /* ---- Fabric's button (DEC-0040, FB-1…FB-6; fabric-button.js) ---- */
201
+ /* FB-2: fixed to the bottom-right (bottom-left right-to-left), 16 px plus the safe area; above the content and the
202
+ sticky chrome (z 10), below the page's dialogs. The page keeps the button's height plus 16 px free at the end. */
203
+ :root { --fd-fb-size: 44px; }
204
+ html.fd-has-fabric-button body { padding-bottom: calc(var(--fd-fb-size) + 2 * var(--space-4) + env(safe-area-inset-bottom, 0px)); }
205
+ .fd-fabric-button-wrap { position: fixed; z-index: 15; bottom: calc(var(--space-4) + env(safe-area-inset-bottom, 0px));
206
+ right: calc(var(--space-4) + env(safe-area-inset-right, 0px)); display: flex; flex-direction: column; align-items: flex-end; gap: var(--space-2); }
207
+ [dir="rtl"] .fd-fabric-button-wrap { right: auto; left: calc(var(--space-4) + env(safe-area-inset-left, 0px)); align-items: flex-start; }
208
+ /* FB-3: the mark and the word, 44 × 44 at least; colours from the theme: --ink on --panel (AA), an --edge boundary (3:1). */
209
+ .fd-fabric-button { display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2); min-width: var(--fd-fb-size);
210
+ min-height: var(--fd-fb-size); padding: 0 var(--space-3); font: 600 var(--t-body) var(--font-ui); color: var(--ink); background: var(--panel);
211
+ border: 2px solid var(--edge); border-radius: var(--r-pill); cursor: pointer;
212
+ transition: background-color var(--dur-hover) var(--motion-ease); }
213
+ .fd-fabric-button:hover { background: var(--panel-2); }
214
+ .fd-fabric-button:focus-visible, .fd-fabric-button-item:focus-visible { outline: 3px solid var(--accent); outline-offset: 2px; }
215
+ .fd-fabric-button-mark { width: 20px; height: 20px; flex: none; }
216
+ @media (max-width: 479.98px) { .fd-fabric-button-label { display: none; } .fd-fabric-button { padding: 0; } }
217
+ /* FB-4: the menu; FB-5: the note when Fabric is absent. */
218
+ .fd-fabric-button-menu { min-width: 240px; max-width: min(360px, calc(100vw - 2 * var(--space-4))); padding: var(--space-1); background: var(--panel);
219
+ color: var(--ink); border: 1px solid var(--edge); border-radius: var(--r-card); }
220
+ .fd-fabric-button-menu[hidden] { display: none; }
221
+ .fd-fabric-button-item { display: block; padding: var(--space-2) var(--space-3); min-height: var(--fd-fb-size); box-sizing: border-box;
222
+ color: var(--ink); text-decoration: none; border-radius: var(--r-control); font: var(--t-body)/1.5 var(--font-ui); overflow-wrap: anywhere; }
223
+ .fd-fabric-button-item:hover, .fd-fabric-button-item:focus { background: var(--panel-2); }
224
+ .fd-fabric-button-note { margin: var(--space-1) 0 0; padding: var(--space-2) var(--space-3); border-top: 1px solid var(--border-strong);
225
+ font: var(--t-body)/1.5 var(--font-ui); color: var(--ink); }
226
+ .fd-fabric-button-note[hidden] { display: none; }
227
+ .fd-fabric-button-get { color: var(--ink); text-decoration: underline; }
@@ -1,4 +1,4 @@
1
- /* fabric-dashboard-kit@1.0.0 · fabric-dashboard/0.1 (Fabric Agent Contract DEC-0036)
1
+ /* fabric-dashboard-kit@1.1.0 · fabric-dashboard/0.1 (Fabric Agent Contract DEC-0036)
2
2
  Load it same-origin with <script src="fabric-dashboard.js" defer></script>: no inline script, no
3
3
  eval, nothing from a CDN. It applies the host's appearance (ED-6, ED-8), folds navigation that
4
4
  does not fit into a More menu (ED-3), makes wide boxes keyboard-reachable (ED-4) and keeps
@@ -8,7 +8,7 @@
8
8
  'use strict';
9
9
 
10
10
  var KIT = 'fabric-dashboard-kit';
11
- var VERSION = '1.0.0';
11
+ var VERSION = '1.1.0';
12
12
  /* fabric-dashboard-kit:theme-ids:begin */
13
13
  var THEMES = ['graphite', 'fabric', 'paper', 'forest', 'ocean', 'ember', 'contrast'];
14
14
  /* fabric-dashboard-kit:theme-ids:end */
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fabric-dashboard-kit",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "profile": "fabric-dashboard/0.1",
5
5
  "decision": "DEC-0036",
6
6
  "declarationKey": "https://fabric.passioncode.ai/agent-contract/extensions/dashboard/0.1",
@@ -15,14 +15,20 @@
15
15
  "contrast"
16
16
  ],
17
17
  "files": {
18
- "fabric-dashboard.css": "3993e6ab5e58d5229bc4dc245aa634fcf88a4f59502e1de90d3d3cfef70528a2",
19
- "fabric-dashboard.js": "bac615a40c85d8c668588528f9e21dac800aa01fefb56b8ee573c30520ce74a7",
18
+ "fabric-dashboard.css": "498571903fac6dfd2d6645283da2f703fa945f410f2fafa269a8c9c322735c3b",
19
+ "fabric-dashboard.js": "100f55d940d4ed60aa8511cee4c65219c93114eb7d7a66639b0e4e4d7aa05f22",
20
+ "fabric-button.js": "4f1e100936ae65af5539594681e66092ae1834a4952da547a67d0558c9fadbbe",
20
21
  "themes.json": "511839f1a1a83eb37fadfd132791474497d5c28c5091700b9aa31ad076e359b5"
21
22
  },
22
23
  "history": {
23
24
  "1.0.0": {
24
25
  "fabric-dashboard.css": "3993e6ab5e58d5229bc4dc245aa634fcf88a4f59502e1de90d3d3cfef70528a2",
25
26
  "fabric-dashboard.js": "bac615a40c85d8c668588528f9e21dac800aa01fefb56b8ee573c30520ce74a7"
27
+ },
28
+ "1.1.0": {
29
+ "fabric-dashboard.css": "498571903fac6dfd2d6645283da2f703fa945f410f2fafa269a8c9c322735c3b",
30
+ "fabric-dashboard.js": "100f55d940d4ed60aa8511cee4c65219c93114eb7d7a66639b0e4e4d7aa05f22",
31
+ "fabric-button.js": "4f1e100936ae65af5539594681e66092ae1834a4952da547a67d0558c9fadbbe"
26
32
  }
27
33
  }
28
34
  }
@@ -11,6 +11,7 @@ promises so a service does not re-derive them.
11
11
  - The rules the page keeps
12
12
  - Using the kit
13
13
  - Themes and the host's appearance
14
+ - Fabric's button
14
15
  - Declaring it
15
16
  - Proving it
16
17
  - Upgrading an existing dashboard
@@ -79,6 +80,34 @@ controls while it is present; an unknown `theme` is `graphite`. `data-fd-appeara
79
80
  what the page applies (declare the same in `appearance`). The page re-reads the cookie when it
80
81
  changes (`cookieStore`) and when it becomes visible again.
81
82
 
83
+ ## Fabric's button
84
+
85
+ The kit ships Fabric's button (contract DEC-0040, FB-7): one floating menu button that opens Fabric on this
86
+ service. Load it after the stylesheet, with ids only:
87
+
88
+ ```html
89
+ <script src="fabric-button.js" defer data-service="example-agent.default" data-service-name="Example Agent"
90
+ data-project="project-a" data-project-name="Project A"></script>
91
+ ```
92
+
93
+ - **The menu (FB-4):** "Chat with Fabric about *Example Agent*" always; "Open a coding session in *Project A*" when
94
+ the page knows its project (`data-runner` picks the runner kind). Arrows move, Escape closes and returns focus.
95
+ - **Placement and look (FB-2, FB-3):** bottom-right (bottom-left right-to-left), 16 px off and clear of the safe
96
+ area, above the sticky chrome and below dialogs; the page keeps its height plus 16 px free at the end of the
97
+ scroll. The mark and the word *Fabric*, the mark alone below 480 px; the accessible name is always *Open Fabric*;
98
+ `--ink` on `--panel` with an `--edge` boundary, the pairs the themes are measured on.
99
+ - **Links (FL-1…FL-6):** built with the kit's port of the contract's `buildFabricLink`; a value outside the grammar
100
+ — or one shaped like a credential — means no button, never a bad link. A press opens a view in Fabric and starts
101
+ nothing (FB-8).
102
+ - **The host (FB-6):** when the host's `fabric_button` cookie says `owner=host`, the page hides its own button.
103
+ - **Fabric absent (FB-5):** when the cookie says `fabric=absent`, or `data-fabric="absent"` (a local service whose
104
+ server checked for Fabric's hub document), or when the page is still visible and focused 1.5 s after a link was
105
+ followed, the menu says "Fabric is not installed — get it at passioncode.ai/fabric". Nothing downloads, nothing
106
+ retries.
107
+
108
+ `window.FabricButton` exposes `buildFabricLink`, `parseFabricLink` and `parseButtonCookie` for a page that needs
109
+ them. The button is optional: a page without the script has no button.
110
+
82
111
  ## Declaring it
83
112
 
84
113
  When the page meets ED-1…ED-6, say so in the well-known document (ED-9):
@@ -125,9 +154,9 @@ operator and update only with their go-ahead** — the page is theirs and may be
125
154
 
126
155
  ## When something is missing
127
156
 
128
- - **No Python for the CLI:** copy the two files from `assets/dashboard-kit/` by hand and compare the
157
+ - **No Python for the CLI:** copy the files from `assets/dashboard-kit/` by hand and compare the
129
158
  first line's version with `kit.json`.
130
- - **The page is built by a framework:** import the two files as static assets; keep them unbundled
159
+ - **The page is built by a framework:** import the files as static assets; keep them unbundled
131
160
  or keep the version marker comment, so `status` can still read the version.
132
161
  - **The page needs a fixed width:** not decided by the profile; declare the `minWidth` it needs
133
162
  (up to 1280) and say why in the README.