kloudy 1.0.1 → 1.0.3

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/AGENTS.md CHANGED
@@ -1,6 +1,8 @@
1
1
  ## For AI agents: how to use Kloudy
2
2
 
3
- The catalog is **kloudy 1.0**: 13,440 bots and 44 modality tags, bundled in this package (npm version 1.0.1). `recommend` / `stack` and `bots ... --offline` read that snapshot (or the local cache if one exists). They do not call the network. Kloudy does not run an LLM to choose bots. Ranking is deterministic: the same goal returns the same JSON.
3
+ The catalog is **kloudy 1.0**: 13,440 bots and 44 modality tags, bundled in this package (npm version 1.0.2). `recommend` / `stack` and `bots ... --offline` read that snapshot (or the local cache if one exists). Kloudy never runs an LLM. Ranking is deterministic: the same goal and the same score overrides return the same JSON.
4
+
5
+ When a network is available, `recommend` may GET `https://kloudy.ai/api/feedback/scores` and cache those overrides for about 24 hours in the Kloudy data directory (`~/.kloudy`, or `KLOUDY_DATA_DIR`). Overrides demote, exclude, or boost a bot after the class tier. `--offline`, a failed request, or a missing scores file leaves ranking on the bundled catalog order.
4
6
 
5
7
  Classes:
6
8
 
@@ -18,18 +20,43 @@ Commands:
18
20
 
19
21
  ```sh
20
22
  npx kloudy@latest recommend "<goal>" --json
21
- npx kloudy@latest stack "<goal>" --json
23
+ npx kloudy@latest recommend "<goal>" --json --limit 5
24
+ npx kloudy@latest stack "<goal>" --json --limit 5 --offline
25
+ npx kloudy@latest feedback --stack <stackId> --bot <botId> --result worked|failed|partial [--reason <short>] [--client <name>]
26
+ npx kloudy@latest feedback --flush
22
27
  npx kloudy@latest bots search <words> --offline
23
28
  npx kloudy@latest bots get <id> --offline
24
29
  npx kloudy@latest agents
25
30
  npx kloudy@latest --help
26
31
  ```
27
32
 
28
- `recommend` and `stack` are aliases. `--json` prints only a JSON object on stdout (`schema`: `kloudy.recommend/1`, `catalog`: `kloudy 1.0`, `version`: the package version). Without `--json` you get the same sections as text, including an Unmatched section when a part did not match.
33
+ `recommend` and `stack` are aliases. `--json` prints only a JSON object on stdout (`schema`: `kloudy.recommend/1`, `catalog`: `kloudy 1.0`, `version`: the package version). Without `--json` you get the same sections as text, including an Unmatched section when a part did not match. `--limit <n>` sets the per-part cap. `n` is an integer from 1 to 20. The default is 5. `--offline` skips the score-override request.
29
34
 
30
35
  Each bot has `id`, `name`, `class` (`A`, `B`, or `O`), `command`, `license`, `description`, `why`, and `modalities`. `command` is what you run or connect to: an `npx ...` command, a `uvx ...` command, or a URL. `recommend` does not install or download anything. Use `bots get <id> --offline` to inspect a pick, then run the command only when you intend to.
31
36
 
32
- The goal is decomposed before matching. Clauses split on commas, semicolons, and the words `and` and `then`. Inside a clause, distinct modality keywords become separate parts. Each part gets the top bots for each modality it needs (up to five). Order is class A, then B, then O, then primary-modality hint, keyword hits, catalog score, and catalog index. A part with no modality or no catalog row is returned in `unmatched` with the part text and the reason `no modality or catalog match`. Parts are not dropped to make a partial stack look complete.
37
+ Write the goal as one sentence per need, or as a comma-separated list of needs. A long sentence is still accepted. Clearer parts produce a clearer stack.
38
+
39
+ The goal is decomposed before matching. Clauses split on commas, semicolons, and the words `and` and `then`. Inside a clause, distinct modality keywords become separate parts. Each part gets the top bots for each modality it needs, then the list is capped per part. Each part includes `candidatesTotal` (how many eligible candidates existed before the cap) and `bots` (the capped list). Narrow that capped list with the project you have open: language, frameworks, licenses, and tools already in the repo. Kloudy never runs an LLM and does not read the project to pick a bot.
40
+
41
+ Familiar modality words such as transcribe, summarize, and database still return the top bots in that modality. A narrower need, such as a watch, a timer, Android sync, a meal plan, or a social post, keeps only bots that share a real keyword or a clearly related catalog name, id, or cap with that part. Unrelated bots are not added as filler. Order is class A, then B, then O, then score override (boost, then unchanged, then demote), then primary-modality hint, keyword hits, catalog score, and catalog index. Exclude removes a bot before the cap. A part with no modality or no catalog row is returned in `unmatched` with the part text and the reason `no modality or catalog match`. Parts are not dropped to make a partial stack look complete.
42
+
43
+ `phone`, `call`, and `calls` map to `audio` only when an audio cue is in the same part or clause: transcribe, transcription, voice, speech, audio, whisper, microphone, asr, phone call, or call audio. Otherwise phone, Android, and mobile are a device-sync need, not audio.
44
+
45
+ Every part has `confidence`: `match`, `weak`, or `none`. Top-level `complete` is true only when every part is `match` and `gaps` is empty. `complete` false means the stack is partial. `gaps` lists unmatched and weak parts. Each gap has the part text, `confidence`, and a reason. `kind` is `catalog integration` when a catalog bot with an install or run command is a real but weak hit. `kind` is `suggestion: build yourself / platform API` when the catalog does not cover the need, such as Wear OS sensor APIs, Health Connect, or the Facebook Graph API. That suggestion is unverified non-catalog guidance. It is not a catalog bot.
46
+
47
+ Every recommend result includes `limit`, `stackId`, and `feedback`. `stackId` is a sha256 of the goal's parts plus the chosen bot ids. The same parts and the same picks always produce the same `stackId`. `feedback` is the command to run after you try a bot. Put that bot's id in place of `<id>`.
48
+
49
+ After you try a stack, report whether it worked:
50
+
51
+ ```sh
52
+ kloudy feedback --stack <stackId> --bot <botId> --result worked|failed|partial [--reason <short>] [--client <name>]
53
+ ```
54
+
55
+ `--result` is `worked`, `failed`, or `partial`. `--reason` is optional and short: it is truncated to 80 characters, and a path, token, or email is dropped. `--client` is an optional tool name such as `cursor`, `claude`, `codex`, or `vscode`. It counts distinct clients. Do not pass a person, email, or account. The report is opt-in and anonymous. The payload may include only `stackId`, `botId`, `result`, an optional short `reason`, the kloudy version, and an optional client name. It never includes the goal text, personal info, file paths, environment, or tokens. The POST goes to `https://kloudy.ai/api/feedback`. If the network is down or the POST fails, the report is queued in the Kloudy data directory and sent on the next `kloudy feedback` or `kloudy feedback --flush`.
56
+
57
+ Score overrides come from those anonymous reports. `scripts/aggregate-feedback-scores.mjs` is meant to run weekly Sunday 3:00 America/Phoenix. It writes `feedback-scores.json`. Bots with fewer than 3 reports are ignored. A bot is demoted after it fails for 3 or more distinct clients with a failure ratio of 60% or more. A bot is excluded after 10 or more reports with a failure ratio of 80% or more. A bot is boosted at 5 or more reports when at least 80% are `worked` and it is not demoted or excluded. `partial` counts as a report and counts as neither worked nor failed. Failure ratio is `failed` divided by all reports. Reports without a client still count toward ratios. They do not add a distinct client.
58
+
59
+ Without `--json`, the text includes Confidence, Candidates, Gaps, `limit`, `stackId`, `feedback`, and `complete=true` or `complete=false`. `--json` still prints only the JSON object (`schema`: `kloudy.recommend/1`, `catalog`: `kloudy 1.0`, `version`: `1.0.2`).
33
60
 
34
61
  Single-part example:
35
62
 
@@ -51,4 +78,6 @@ Three parts:
51
78
  2. `summarizes them` — `text` — `httpk:mcp:ai.liquidagent.api:liquid-agent` — `https://api.liquidagent.ai/mcp`
52
79
  3. `stores them in a database` — `database_access` — `httpk:npm:microsoft:postgres-mcp` — `npx -y @microsoft/postgres-mcp`
53
80
 
54
- `weave a basket` in a larger goal is unmatched. It is reported, not omitted.
81
+ That control stack is `complete` true: each part is confidence `match`, and `gaps` is empty.
82
+
83
+ `weave a basket` in a larger goal is unmatched. It is reported, not omitted. The goal is then `complete` false.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Kloudy — add it to your editor
1
+ # Kloudy — add it to your editor
2
2
 
3
3
  Connect your MCP client to https://kloudy.ai/mcp for the current remote catalog.
4
4
 
@@ -14,6 +14,16 @@ No arguments detects Cursor, VS Code, Claude Desktop, Claude Code, Windsurf and
14
14
 
15
15
  The remote endpoint is https://kloudy.ai/mcp. Public catalog discovery works without a key and makes no model call. It returns up to five public metadata candidates, not execution grants. Account actions still require an existing authorized Kloudy key; self-service engine-key issuance is not connected. If you already have one, set `KLOUDY_MCP_TOKEN` privately in your environment; the installer asks before storing it in a client's headers (or Claude Desktop's stdio environment). Keys are never accepted as command-line arguments or printed by the installer.
16
16
 
17
+ For an existing scoped assistant connection, select its private file explicitly:
18
+
19
+ ```sh
20
+ npx kloudy@latest mcp --connection /private/path/connection.json
21
+ ```
22
+
23
+ `KLOUDY_CONNECTION` is the equivalent private-file environment setting. The file must contain `endpoint` equal to `https://kloudy.ai/mcp`, a valid `access_token`, and a future Unix-seconds `expires_at`. A file and `KLOUDY_MCP_TOKEN` cannot be combined. The remote proxy never silently reads a default connection file, issues permission, or upgrades a discovery key.
24
+
25
+ Authenticated room messaging is available through the existing `kloudy` tool's `operation: "room"` with `room_operation: "send"`, `"inbox"`, or `"reply"` and the corresponding schema. The backend validates the exact project, authorized client and recipient. These are data transport operations: a queued message is not an assistant response, and installing this package does not enable unattended assistant turns or voice calls. Server enrollment and account permissions must already support the selected connection.
26
+
17
27
  `mcp` is a local stdio-to-Streamable-HTTP proxy; stdout contains MCP messages only. Direct questions and the proxy call the same remote server. Network failures and invalid keys fail explicitly. Resources are forwarded whenever the upstream supports them, including with autoupdate off. Catalog change notifications and a single reconnect after an explicit session-expired HTTP 404 remain gated by `KLOUDY_MCP_AUTOUPDATE=meta` or `notify`; the default is `off`. Ambiguous tool failures are never retried.
18
28
 
19
29
  Interactive cards require a host that supports MCP Apps. The proxy relays the supported Apps MIME capability; it does not claim sampling, roots or elicitation. Hosts without Apps can use named text choices, source links and readable resources. The one-shot CLI prints text rather than rendering an interactive card. When the hosted catalog offers visual setup choices, the IDE uses its own available tools and account terms to make a local mockup; Kloudy supplies data and does not invoke backend model/image generation. Resource hosting still has operational cost. Selecting a layout does not authorize installation, account connection, paid image APIs or deployment.
@@ -24,13 +34,15 @@ Offline bot catalog: `npx kloudy bots list|search|get|sync|works|broken|modaliti
24
34
 
25
35
  ## For AI agents: how to use Kloudy
26
36
 
27
- The catalog is **kloudy 1.0**: 13,440 bots and 44 modality tags, shipped in this package (npm version 1.0.1). Recommend and the offline bots commands read that bundled snapshot, then the local cache. They do not call the network, and Kloudy does not run an LLM to pick a stack.
37
+ The catalog is **kloudy 1.0**: 13,440 bots and 44 modality tags, shipped in this package (npm version 1.0.3). Recommend and the offline bots commands read that bundled snapshot, then the local cache. Kloudy never runs an LLM. Ranking is deterministic: the same goal and the same score overrides return the same JSON.
38
+
39
+ When a network is available, `recommend` may GET `https://kloudy.ai/api/feedback/scores` and cache those overrides for about 24 hours in the Kloudy data directory (`~/.kloudy`, or `KLOUDY_DATA_DIR`). Overrides demote, exclude, or boost a bot after the class tier. `--offline`, a failed request, or a missing scores file leaves ranking on the bundled catalog order.
28
40
 
29
41
  Classes:
30
42
 
31
- - **A** — live HTTP MCP endpoints.
32
- - **B** — npx or uvx packages.
33
- - **O** — open source and Hugging Face listings.
43
+ - **A** — live HTTP MCP endpoints.
44
+ - **B** — npx or uvx packages.
45
+ - **O** — open source and Hugging Face listings.
34
46
 
35
47
  List the modality tags:
36
48
 
@@ -38,18 +50,43 @@ List the modality tags:
38
50
  npx kloudy@latest bots modalities --offline
39
51
  ```
40
52
 
41
- Turn a plain-language goal into a stack. `recommend` and `stack` are the same command. Add `--json` when you need parseable output (`schema` is `kloudy.recommend/1`).
53
+ Turn a plain-language goal into a stack. `recommend` and `stack` are the same command. Add `--json` when you need parseable output (`schema` is `kloudy.recommend/1`). `--limit <n>` caps each part (`n` is an integer from 1 to 20; the default is 5). `--offline` skips the score-override request.
42
54
 
43
55
  ```sh
44
56
  npx kloudy@latest recommend "<goal>" --json
45
- npx kloudy@latest stack "<goal>" --json
57
+ npx kloudy@latest recommend "<goal>" --json --limit 5
58
+ npx kloudy@latest stack "<goal>" --json --limit 5 --offline
59
+ npx kloudy@latest feedback --stack <stackId> --bot <botId> --result worked|failed|partial [--reason <short>] [--client <name>]
60
+ npx kloudy@latest feedback --flush
46
61
  npx kloudy@latest bots search <words> --offline
47
62
  npx kloudy@latest bots get <id> --offline
48
63
  ```
49
64
 
50
65
  Each recommended bot includes `id`, `class` (A, B, or O), `command`, `license`, a one-line `description`, and `why`. `command` is an `npx` or `uvx` invocation, or a URL for a live endpoint or source repo. Run or open that command when you want to install or connect. `recommend` itself does not install anything. Inspect a pick with `bots get` before running it.
51
66
 
52
- `recommend` splits the goal on commas, semicolons, and the words `and` and `then`, and it also splits a clause when keywords hit different modalities. You get one section per part, with the top bots in each needed modality (class A, then B, then O, then the same primary-modality, score, and index ordering as catalog search). A part with no modality or no catalog row stays in `unmatched` with the part text and a reason. It is not dropped.
67
+ Write the goal as one sentence per need, or as a comma-separated list of needs. A long sentence is still accepted. Clearer parts produce a clearer stack.
68
+
69
+ `recommend` splits the goal on commas, semicolons, and the words `and` and `then`, and it also splits a clause when keywords hit different modalities. You get one section per part. Each part includes `candidatesTotal` (how many eligible candidates existed before the cap) and `bots` (the capped list). Narrow that capped list with the project you have open: language, frameworks, licenses, and tools already in the repo. Kloudy never runs an LLM and does not read the project to pick a bot.
70
+
71
+ Order is class A, then B, then O, then score override (boost, then unchanged, then demote), then the same primary-modality, keyword, score, and index ordering as catalog search. Exclude removes a bot before the cap. Familiar modality words such as transcribe, summarize, and database still return the top bots in that modality. A narrower need, such as a watch, a timer, Android sync, a meal plan, or a social post, keeps only bots that share a real keyword or a clearly related catalog name, id, or cap with that part. Unrelated bots are not filler. A part with no modality or no catalog row stays in `unmatched` with the part text and a reason. It is not dropped.
72
+
73
+ `phone`, `call`, and `calls` are `audio` only when an audio cue is in the same part or clause (transcribe, transcription, voice, speech, audio, whisper, microphone, asr, phone call, or call audio). Otherwise phone, Android, and mobile are device sync, not audio.
74
+
75
+ Each part includes `confidence` (`match`, `weak`, or `none`). Top-level `complete` is true only when every part is `match` and `gaps` is empty. `complete` false means the stack is partial. `gaps` lists unmatched and weak parts with the part text, confidence, and a reason. `kind` `catalog integration` means a catalog bot with an install or run command is a real but weak hit. `kind` `suggestion: build yourself / platform API` means the catalog does not cover the need (Wear OS sensor APIs, Health Connect, or the Facebook Graph API, for example). That suggestion is unverified non-catalog guidance, not a catalog bot.
76
+
77
+ Every recommend result includes `limit`, `stackId`, and `feedback`. `stackId` is a sha256 of the goal's parts plus the chosen bot ids. The same parts and the same picks always produce the same `stackId`. `feedback` is the command to run after you try a bot. Put that bot's id in place of `<id>`.
78
+
79
+ After you try a stack, report whether it worked:
80
+
81
+ ```sh
82
+ kloudy feedback --stack <stackId> --bot <botId> --result worked|failed|partial [--reason <short>] [--client <name>]
83
+ ```
84
+
85
+ `--result` is `worked`, `failed`, or `partial`. `--reason` is optional and short: it is truncated to 80 characters, and a path, token, or email is dropped. `--client` is an optional tool name such as `cursor`, `claude`, `codex`, or `vscode`. It counts distinct clients. Do not pass a person, email, or account. The report is opt-in and anonymous. The payload may include only `stackId`, `botId`, `result`, an optional short `reason`, the kloudy version, and an optional client name. It never includes the goal text, personal info, file paths, environment, or tokens. The POST goes to `https://kloudy.ai/api/feedback`. If the network is down or the POST fails, the report is queued in the Kloudy data directory and sent on the next `kloudy feedback` or `kloudy feedback --flush`.
86
+
87
+ Score overrides come from those anonymous reports. `scripts/aggregate-feedback-scores.mjs` is meant to run weekly Sunday 3:00 America/Phoenix. It writes `feedback-scores.json`. Bots with fewer than 3 reports are ignored. A bot is demoted after it fails for 3 or more distinct clients with a failure ratio of 60% or more. A bot is excluded after 10 or more reports with a failure ratio of 80% or more. A bot is boosted at 5 or more reports when at least 80% are `worked` and it is not demoted or excluded. `partial` counts as a report and counts as neither worked nor failed. Failure ratio is `failed` divided by all reports. Reports without a client still count toward ratios. They do not add a distinct client.
88
+
89
+ Text output includes Confidence, Candidates, Gaps, `limit`, `stackId`, `feedback`, and `complete=true` or `complete=false`. `--json` still prints only the JSON object.
53
90
 
54
91
  Single-part example:
55
92
 
@@ -57,7 +94,7 @@ Single-part example:
57
94
  npx kloudy@latest recommend "voice assistant that transcribes phone calls" --json
58
95
  ```
59
96
 
60
- That goal is one audio part. The first of the five bots is `httpk:npm:mendelio-voice-mcp` (class B, MIT, `npx -y mendelio-voice-mcp`).
97
+ That goal is one audio part. The first bot in the capped list is `httpk:npm:mendelio-voice-mcp` (class B, MIT, `npx -y mendelio-voice-mcp`).
61
98
 
62
99
  Multi-part example:
63
100
 
@@ -67,11 +104,13 @@ npx kloudy@latest recommend "voice assistant that transcribes calls, summarizes
67
104
 
68
105
  That becomes three parts, not one blended search:
69
106
 
70
- 1. `voice assistant that transcribes calls` — `audio` — `httpk:npm:mendelio-voice-mcp` — `npx -y mendelio-voice-mcp`
71
- 2. `summarizes them` — `text` — `httpk:mcp:ai.liquidagent.api:liquid-agent` — `https://api.liquidagent.ai/mcp`
72
- 3. `stores them in a database` — `database_access` — `httpk:npm:microsoft:postgres-mcp` — `npx -y @microsoft/postgres-mcp`
107
+ 1. `voice assistant that transcribes calls` — `audio` — `httpk:npm:mendelio-voice-mcp` — `npx -y mendelio-voice-mcp`
108
+ 2. `summarizes them` — `text` — `httpk:mcp:ai.liquidagent.api:liquid-agent` — `https://api.liquidagent.ai/mcp`
109
+ 3. `stores them in a database` — `database_access` — `httpk:npm:microsoft:postgres-mcp` — `npx -y @microsoft/postgres-mcp`
110
+
111
+ That control stack is complete (`complete` true). Each part is confidence `match`.
73
112
 
74
- A leftover clause such as `weave a basket` is listed under Unmatched (`no modality or catalog match`) instead of disappearing.
113
+ A leftover clause such as `weave a basket` is listed under Unmatched (`no modality or catalog match`) and under Gaps. The goal is then `complete` false.
75
114
 
76
115
  On first contact, read the agent guide and the command list:
77
116
 
@@ -98,7 +137,7 @@ npm start
98
137
  npm run card -- /world
99
138
  ```
100
139
 
101
- `init` creates a new folder and refuses to overwrite an existing one. `kloudy read https://example.com` and `kloudy card /world` also work directly. These are real public reads, with no model or account call; shared rate limits and website restrictions apply. The public Node exports are `readPublicPage` and `readCard` from `kloudy/public`. Source HTML stays data, never executable UI. The starter’s separate toolbox example requires an existing private connection and selects definitions without executing them.
140
+ `init` creates a new folder and refuses to overwrite an existing one. `kloudy read https://example.com` and `kloudy card /world` also work directly. These are real public reads, with no model or account call; shared rate limits and website restrictions apply. The public Node exports are `readPublicPage` and `readCard` from `kloudy/public`. Source HTML stays data, never executable UI. The starter’s separate toolbox example requires an existing private connection and selects definitions without executing them.
102
141
 
103
142
  Developer guide and availability: https://kloudy.ai/build. Customer self-registration is separate from these working public examples.
104
143
 
@@ -140,7 +179,7 @@ One user installation covers projects. Cursor, Claude Code, VS Code and Codex re
140
179
 
141
180
  Bare `kloudy`, `Kloudy`, `/kloudy`, and `ask Kloudy` in this CLI return a project-aware introduction before opening an engine session. The MCP tool does the same for `introduce` or an exact attach intent. It classifies known manifest names/dependencies, looks up at most three relevant public catalog names, and asks one install/connect question. Catalog candidates are explicitly non-executable until granted. It never reads `.env`, source contents or another project's checkpoint. With an HTTPK connection, the introduction comes from the real engine's `bbe.attach.v1` intent payload. Without a reachable account/session, the client provides a clearly marked deterministic public-catalog fallback. An unconfigured IDE cannot discover a CLI just from an ordinary chat word: install the MCP adapter first, then invoke the tool or `/kloudy` prompt.
142
181
 
143
- The remote option stores only a bearer **environment-variable reference**. `https://kloudy.ai/mcp` now serves authenticated MCP alongside its human guide. The current hosted engine is a finite development service with separately scoped Notes and bounded Base chain-head read clients; it is not open customer registration. The default local stdio adapter works without hosting. With no explicit connection path, all three clients use `~/.kloudy/connection.json`, the same user session across projects.
182
+ The remote option stores only a bearer **environment-variable reference**. `https://kloudy.ai/mcp` now serves authenticated MCP alongside its human guide. The current hosted engine is a finite development service with separately scoped Notes and bounded Base chain-head read clients; it is not open customer registration. The default local stdio adapter works without hosting. With no explicit connection path, the legacy CLI, legacy MCP and SDK use `~/.kloudy/connection.json`. The remote `kloudy mcp` proxy uses only an explicit connection file or environment token, as described above.
144
183
 
145
184
  The MCP tool list initially has one `kloudy` entry. An `ask` operation adds only the selected tool as `selected_<engine-name>`. Selecting a new task replaces it; running it clears the selection. Status/cancellation use the Kloudy entry and the same engine request. Model-visible outputs never contain the bearer.
146
185
 
@@ -170,7 +209,7 @@ MCP transport compatibility is pinned to the official SDK 1.31.0 and its 2025-11
170
209
 
171
210
  `npm test` covers real HTTP/stdio round trips, selected-tool isolation, auth/origin rejection, crash handling, shared SDK behavior, explicit approval and safe IDE installation.
172
211
 
173
- The real-engine acceptance script uses a separately provisioned local Blackbox instance and synthetic public metadata, never a mock engine. It exercises CLI → MCP → SDK → autonomous completed Notes result, with no extra approval call. It intentionally requires an explicit test flag:
212
+ The real-engine acceptance script uses a separately provisioned local Blackbox instance and synthetic public metadata, never a mock engine. It exercises CLI → MCP → SDK → autonomous completed Notes result, with no extra approval call. It intentionally requires an explicit test flag:
174
213
 
175
214
  ```sh
176
215
  node scripts/prove.mjs --connection /private/test-connection.json --candidates /path/to/test-cards.json --approve-synthetic
@@ -194,9 +233,9 @@ A standard remote MCP host sees one `kloudy` tool. `introduce` gets the engine g
194
233
 
195
234
  `node scripts/prove-hosted.mjs` uses an in-memory, expiring test grant and a temporary user config to prove the real public MCP handshake, engine greeting, one selected tool, completed Notes receipt, idempotent replay and SDK readback. It does not claim a visual IDE session or the provider-model failure ladder was tested. Those remaining checks are recorded in Shared.
196
235
 
197
- ## Selected Toolboxes — 0.3.1
236
+ ## Selected Toolboxes — 0.3.1
198
237
 
199
- With a scoped HTTPK connection, each ask returns its engine selection_id. Collect 1–16 distinct IDs from that same session. Assembly is passive: no model, action or approval runs.
238
+ With a scoped HTTPK connection, each ask returns its engine selection_id. Collect 1–16 distinct IDs from that same session. Assembly is passive: no model, action or approval runs.
200
239
 
201
240
  ```sh
202
241
  kloudy ask "read note NOTE_ID" --connection /private/httpk.json
@@ -216,7 +255,7 @@ Open 0.3.4 adds `HttpkClient.confirm({selectionId, token, decision: 'approve', s
216
255
 
217
256
  The same public MCP gateway accepts `operation: 'confirm'`, `selection_id`, `decision: 'approve'`, `source`, and exactly one of `token` or `revision`. Missing binding/source fails closed. `scripts/prove-confirmation.mjs` verifies a real bounded Base read, wrong-token denial, zero execution before approval and receipt replay using an existing finite grant from memory. No provider key is distributed.
218
257
 
219
- ## IDE experience — 0.3.4
258
+ ## IDE experience — 0.3.4
220
259
 
221
260
  An already attached MCP host receives a welcome and current-project guidance, not another install question. Kloudy or /kloudy explicitly addresses the service; the IDE may use the same tools for relevant tasks without a prefix. The current task selects a small toolbox; a persistent account connection does not load its tools into every project.
222
261
 
@@ -235,7 +274,7 @@ const list = await convertPages(['https://example.com', 'https://example.com/mis
235
274
  console.log(list.cards, list.reports); // Each unreadable source is reported.
236
275
  ```
237
276
 
238
- MCP's single `kloudy` door also accepts `operation: "convert", url` or `operation: "yield_list", urls` (1–8). No account is needed for these public reads. The engine owns KLDY query matching and ranking. This client returns content and per-source conversion reports, never a ranked recommendation or permission to execute tools.
277
+ MCP's single `kloudy` door also accepts `operation: "convert", url` or `operation: "yield_list", urls` (1–8). No account is needed for these public reads. The engine owns KLDY query matching and ranking. This client returns content and per-source conversion reports, never a ranked recommendation or permission to execute tools.
239
278
 
240
279
  Cards validate against the unmodified SDF v0.2 root schema pinned to `sdfprotocol/sdf@e4619ad0b951ced8542692c6b624cb86794cb863` and the additional Kloudy webpage profile. This upstream machine-readable schema differs from its website examples: it uses `sdf:<sha256>`, `summary.brief`, `source.fetched_at`, and section `title`/`summary`. Schemas and upstream MIT notice ship in `schemas/` and at [kloudy.ai/schemas/sdf-webpage-v1.schema.json](https://kloudy.ai/schemas/sdf-webpage-v1.schema.json). `article.x-kloudy-webpage` is our explicit extractive subtype, not a claim that an arbitrary page is a news article. Empty entities mean no semantic entity extraction was performed.
241
280
 
@@ -345,7 +384,7 @@ Sources: https://docs.ollama.com/api/tags ; https://docs.ollama.com/api/openai-c
345
384
 
346
385
  ### Host interface and deterministic replies
347
386
 
348
- HTTPK initialization negotiates chat/text/speech presentation with BBE 0.3.50. It never changes scopes, the credential surface or the approval policy. Older engines remain usable without invented negotiation. `renderReply(state,{action})` from `kloudy/replies` renders bounded JSON templates for observed operation state. Unknown state stays unconfirmed; output is plain text. The browser uses the same templates in its one conversation. Kloudy’s subscription NVIDIA ladder is the text/planning upgrade; supported local Siri/Nano remains voice. Bot Boozle is a separate optional IDE.
387
+ HTTPK initialization negotiates chat/text/speech presentation with BBE 0.3.50. It never changes scopes, the credential surface or the approval policy. Older engines remain usable without invented negotiation. `renderReply(state,{action})` from `kloudy/replies` renders bounded JSON templates for observed operation state. Unknown state stays unconfirmed; output is plain text. The browser uses the same templates in its one conversation. Kloudy’s subscription NVIDIA ladder is the text/planning upgrade; supported local Siri/Nano remains voice. Bot Boozle is a separate optional IDE.
349
388
 
350
389
  ## Encrypted conversation client
351
390
 
package/bin/entry.mjs CHANGED
@@ -1,25 +1,32 @@
1
- #!/usr/bin/env node
2
- import {readFileSync} from 'node:fs';
3
- const version=JSON.parse(readFileSync(new URL('../package.json',import.meta.url),'utf8')).version;
4
- import {createInterface} from 'node:readline/promises';
5
- import {installEditors} from '../src/editor-install.mjs';
6
- import {askRemote,serveRemote} from '../src/remote.mjs';
7
- const args=process.argv.slice(2);
8
- try{
9
- if(!args.length||args[0]==='install'&&args.length===1){
10
- console.log('Kloudy · Add to your editor\nFree public discovery. Account actions require an authorized Kloudy key.');
11
- let rl;
12
- const confirm=async({client,path,authenticated})=>{if(!process.stdin.isTTY){console.log(`${client}: skipped — run in an interactive terminal to approve ${path}`);return false;}rl??=createInterface({input:process.stdin,output:process.stdout});return /^(y|yes)$/i.test((await rl.question(`Add Kloudy to ${client}? ${path}${authenticated?' (stores your key)':''} [y/N] `)).trim());};
13
- let results;try{results=await installEditors({confirm,key:process.env.KLOUDY_MCP_TOKEN,onResult:r=>console.log(`${r.client}: ${r.state}`)});}finally{rl?.close();}
14
- if(!results.length)console.log('No supported editor configuration found. Open https://kloudy.ai/install for setup help.');
15
- else console.log('Existing servers were preserved. Restart configured clients to connect.');
16
- if(results.some(r=>r.state==='unchanged_error'))process.exitCode=1;
17
- }else if(args[0]==='mcp'&&args.length===1){await serveRemote();}
18
- else if(args[0]==='--version'){console.log(version);}
19
- else if(['--help','help'].includes(args[0]))console.log('npx kloudy Add to detected editors (asks before each write)\nnpx kloudy <question> Ask the public Kloudy MCP endpoint\nnpx kloudy mcp Remote MCP over local stdio\nnpx kloudy recommend "<goal>" [--json]\nnpx kloudy stack "<goal>" [--json]\nnpx kloudy agents\nnpx kloudy bots list|search|get|sync|works|broken|modalities [--offline]\nnpx kloudy legacy <args> Existing SDK/engine CLI commands\n\nFree discovery needs no key. For authorized account actions set KLOUDY_MCP_TOKEN privately; never put a key in command arguments. Node 20.19+ required.');
20
- else if(args[0]==='bots'){const {botsCommand}=await import('../src/bots.mjs');try{await botsCommand(args.slice(1));}catch(error){process.stderr.write((error?.message||'bots failed')+'\n');process.exitCode=1;}}
21
- else if(args[0]==='recommend'||args[0]==='stack'){const {recommendCommand}=await import('../src/recommend.mjs');try{await recommendCommand(args.slice(1));}catch(error){process.stderr.write((error?.message||'recommend failed')+'\n');process.exitCode=1;}}
22
- else if(args[0]==='agents'||args[0]==='agent'){const {agentsCommand}=await import('../src/recommend.mjs');try{agentsCommand();}catch(error){process.stderr.write((error?.message||'agents failed')+'\n');process.exitCode=1;}}
23
- else if(args[0]==='legacy'){process.argv.splice(2,1);await import('./kloudy.mjs');}
24
- else{if(args[0]?.startsWith('--'))throw Error('Unknown option');const result=await askRemote(args.join(' '));for(const c of result.content||[])if(c.type==='text')process.stdout.write((process.env.KLOUDY_MCP_TOKEN?c.text.replaceAll(process.env.KLOUDY_MCP_TOKEN,'[redacted]'):c.text)+'\n');if(result.isError)process.exitCode=1;}
25
- }catch{process.stderr.write('Kloudy could not complete the request. Check your connection or authorized key. No action was retried. Run npx kloudy --help.\n');process.exitCode=1;}
1
+ #!/usr/bin/env node
2
+ import {readFileSync} from 'node:fs';
3
+ const version=JSON.parse(readFileSync(new URL('../package.json',import.meta.url),'utf8')).version;
4
+ import {createInterface} from 'node:readline/promises';
5
+ import {installEditors} from '../src/editor-install.mjs';
6
+ import {askRemote,serveRemote} from '../src/remote.mjs';
7
+ const args=process.argv.slice(2);
8
+ function remoteArgs(values){
9
+ const words=[...values],indexes=words.flatMap((value,index)=>value==='--connection'?[index]:[]);let options={};
10
+ if(indexes.length){const i=indexes[0],file=words[i+1];if(indexes.length!==1||typeof file!=='string'||!file.trim()||file.startsWith('--'))throw Error('Private connection path required');options={connection:file};words.splice(i,2);}
11
+ if(words.some(value=>value.startsWith('--')))throw Error('Unknown option');
12
+ return {words,options};
13
+ }
14
+ try{
15
+ if(!args.length||args[0]==='install'&&args.length===1){
16
+ console.log('Kloudy · Add to your editor\nFree public discovery. Account actions require an authorized Kloudy key.');
17
+ let rl;
18
+ const confirm=async({client,path,authenticated})=>{if(!process.stdin.isTTY){console.log(`${client}: skipped — run in an interactive terminal to approve ${path}`);return false;}rl??=createInterface({input:process.stdin,output:process.stdout});return /^(y|yes)$/i.test((await rl.question(`Add Kloudy to ${client}? ${path}${authenticated?' (stores your key)':''} [y/N] `)).trim());};
19
+ let results;try{results=await installEditors({confirm,key:process.env.KLOUDY_MCP_TOKEN,onResult:r=>console.log(`${r.client}: ${r.state}`)});}finally{rl?.close();}
20
+ if(!results.length)console.log('No supported editor configuration found. Open https://kloudy.ai/install for setup help.');
21
+ else console.log('Existing servers were preserved. Restart configured clients to connect.');
22
+ if(results.some(r=>r.state==='unchanged_error'))process.exitCode=1;
23
+ }else if(args[0]==='mcp'){const {words,options}=remoteArgs(args.slice(1));if(words.length)throw Error('Unexpected MCP arguments');await serveRemote(options);}
24
+ else if(args[0]==='--version'){console.log(version);}
25
+ else if(['--help','help'].includes(args[0]))console.log('npx kloudy Add to detected editors (asks before each write)\nnpx kloudy <question> Ask the public Kloudy MCP endpoint\nnpx kloudy mcp [--connection PRIVATE_FILE] Remote MCP over local stdio\nnpx kloudy <question> --connection PRIVATE_FILE Use an existing scoped connection\nnpx kloudy recommend "<goal>" [--json] [--limit N] [--offline]\nnpx kloudy stack "<goal>" [--json] [--limit N] [--offline]\nnpx kloudy feedback --stack ID --bot ID --result worked|failed|partial [--reason TEXT] [--client NAME] [--flush]\nnpx kloudy agents\nnpx kloudy bots list|search|get|sync|works|broken|modalities [--offline]\nnpx kloudy legacy <args> Existing SDK/engine CLI commands\n\nFree discovery needs no key. Remote MCP accepts KLOUDY_CONNECTION or --connection for a private scoped connection file, or the existing KLOUDY_MCP_TOKEN. Choose one credential source; no file is read implicitly. This does not issue grants. Never put a key in command arguments. Node 20.19+ required.');
26
+ else if(args[0]==='bots'){const {botsCommand}=await import('../src/bots.mjs');try{await botsCommand(args.slice(1));}catch(error){process.stderr.write((error?.message||'bots failed')+'\n');process.exitCode=1;}}
27
+ else if(args[0]==='recommend'||args[0]==='stack'){const {recommendCommand}=await import('../src/recommend.mjs');try{await recommendCommand(args.slice(1));}catch(error){process.stderr.write((error?.message||'recommend failed')+'\n');process.exitCode=1;}}
28
+ else if(args[0]==='feedback'){const {feedbackCommand}=await import('../src/feedback.mjs');try{await feedbackCommand(args.slice(1));}catch(error){process.stderr.write((error?.message||'feedback failed')+'\n');process.exitCode=1;}}
29
+ else if(args[0]==='agents'||args[0]==='agent'){const {agentsCommand}=await import('../src/recommend.mjs');try{agentsCommand();}catch(error){process.stderr.write((error?.message||'agents failed')+'\n');process.exitCode=1;}}
30
+ else if(args[0]==='legacy'){process.argv.splice(2,1);await import('./kloudy.mjs');}
31
+ else{if(args[0]==='install')throw Error('Remote connection options do not install editors');const {words,options}=remoteArgs(args);if(!words.length)throw Error('Question required');const result=await askRemote(words.join(' '),options);for(const c of result.content||[])if(c.type==='text')process.stdout.write((process.env.KLOUDY_MCP_TOKEN?c.text.replaceAll(process.env.KLOUDY_MCP_TOKEN,'[redacted]'):c.text)+'\n');if(result.isError)process.exitCode=1;}
32
+ }catch{process.stderr.write('Kloudy could not complete the request. Check your connection or authorized key. No action was retried. Run npx kloudy --help.\n');process.exitCode=1;}
package/bin/kloudy.mjs CHANGED
@@ -19,7 +19,7 @@ try{
19
19
  const connection=option('connection')||process.env.KLOUDY_CONNECTION;
20
20
  const project=option('project')||process.cwd(),decision=option('decision');
21
21
  if(command==='help'||command==='--help'){
22
- console.log('Ask for Kloudy. One CLI, one MCP, tools appear when needed.\n\nkloudy models list [--endpoint URL] [--key-env NAME]\nkloudy models run NAME --prompt-file FILE [--max-tokens 128] [--allow-provider-charge]\nkloudy init my-app\nkloudy read https://example.com\nkloudy card /world\nkloudy convert https://example.com\nkloudy query "need" --connection FILE\nkloudy ask "goal" --connection FILE [--candidates FILE]\nkloudy assemble --selections FILE --connection FILE\nkloudy call NAME --inputs FILE [--selection ID] --connection FILE\nkloudy action chain.head --inputs FILE --connection FILE [--idempotency-key KEY]\nkloudy wallet-connect ethereum --connection FILE\nkloudy hosting [--budget-micros 100000000] --connection FILE\nkloudy wallet balance --connection FILE\nkloudy wallet top_up --amount 10.00 --connection FILE\nkloudy conversation read|append|sync --inputs FILE --connection FILE\nkloudy host --connection FILE\nkloudy status --connection FILE\nkloudy cancel --connection FILE\nkloudy mcp --connection FILE\nkloudy introduce [--project DIR] [--decision yes|no]\nkloudy install [--ide cursor|claude|vscode|codex] [--url HTTPS_URL] [--token-env NAME] [--connection FILE]\nkloudy wrap --config FILE [--port 8796]\nkloudy strings save NAME --selections FILE\nkloudy strings use NAME\nkloudy strings call NAME --tool ALIAS --inputs FILE\nkloudy strings list|prune\nkloudy strings keep|remove NAME\nkloudy login\nkloudy recommend <goal> [--json]\nkloudy stack <goal> [--json]\nkloudy agents\nkloudy bots list|search|get|sync|works|broken|modalities [--offline]\n\nRegister with email + short 2FA through the Core account service. Existing engine connections remain usable. Hosted registration is not supplied by this client package.');
22
+ console.log('Ask for Kloudy. One CLI, one MCP, tools appear when needed.\n\nkloudy models list [--endpoint URL] [--key-env NAME]\nkloudy models run NAME --prompt-file FILE [--max-tokens 128] [--allow-provider-charge]\nkloudy init my-app\nkloudy read https://example.com\nkloudy card /world\nkloudy convert https://example.com\nkloudy query "need" --connection FILE\nkloudy ask "goal" --connection FILE [--candidates FILE]\nkloudy assemble --selections FILE --connection FILE\nkloudy call NAME --inputs FILE [--selection ID] --connection FILE\nkloudy action chain.head --inputs FILE --connection FILE [--idempotency-key KEY]\nkloudy wallet-connect ethereum --connection FILE\nkloudy hosting [--budget-micros 100000000] --connection FILE\nkloudy wallet balance --connection FILE\nkloudy wallet top_up --amount 10.00 --connection FILE\nkloudy conversation read|append|sync --inputs FILE --connection FILE\nkloudy host --connection FILE\nkloudy status --connection FILE\nkloudy cancel --connection FILE\nkloudy mcp --connection FILE\nkloudy introduce [--project DIR] [--decision yes|no]\nkloudy install [--ide cursor|claude|vscode|codex] [--url HTTPS_URL] [--token-env NAME] [--connection FILE]\nkloudy wrap --config FILE [--port 8796]\nkloudy strings save NAME --selections FILE\nkloudy strings use NAME\nkloudy strings call NAME --tool ALIAS --inputs FILE\nkloudy strings list|prune\nkloudy strings keep|remove NAME\nkloudy login\nkloudy recommend <goal> [--json] [--limit N] [--offline]\nkloudy stack <goal> [--json] [--limit N] [--offline]\nkloudy feedback --stack ID --bot ID --result worked|failed|partial [--reason TEXT] [--client NAME] [--flush]\nkloudy agents\nkloudy bots list|search|get|sync|works|broken|modalities [--offline]\n\nRegister with email + short 2FA through the Core account service. Existing engine connections remain usable. Hosted registration is not supplied by this client package.');
23
23
  }else if(command==='models'){
24
24
  const action=args.shift(),endpoint=option('endpoint'),keyEnv=option('key-env'),file=option('prompt-file'),max=option('max-tokens');
25
25
  const allow=args.includes('--allow-provider-charge');if(allow)args.splice(args.indexOf('--allow-provider-charge'),1);
@@ -54,6 +54,9 @@ try{
54
54
  }else if(command==='recommend'||command==='stack'){
55
55
  const {recommendCommand}=await import('../src/recommend.mjs');
56
56
  try{await recommendCommand(args);}catch(error){process.stderr.write((error?.message||'recommend failed')+'\n');process.exitCode=1;}
57
+ }else if(command==='feedback'){
58
+ const {feedbackCommand}=await import('../src/feedback.mjs');
59
+ try{await feedbackCommand(args);}catch(error){process.stderr.write((error?.message||'feedback failed')+'\n');process.exitCode=1;}
57
60
  }else if(command==='agents'||command==='agent'){
58
61
  const {agentsCommand}=await import('../src/recommend.mjs');
59
62
  try{agentsCommand();}catch(error){process.stderr.write((error?.message||'agents failed')+'\n');process.exitCode=1;}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kloudy",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Ask for Kloudy: CLI, MCP and SDK clients for the same scoped engine session.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -39,7 +39,7 @@
39
39
  ],
40
40
  "scripts": {
41
41
  "test": "node --test test/*.test.mjs",
42
- "check": "node --check bin/kloudy.mjs && node --check bin/entry.mjs && node --check src/bots.mjs && node --check src/recommend.mjs && node --check src/client.mjs && npm test",
42
+ "check": "node --check bin/kloudy.mjs && node --check bin/entry.mjs && node --check src/bots.mjs && node --check src/recommend.mjs && node --check src/scores.mjs && node --check src/feedback.mjs && node --check scripts/aggregate-feedback-scores.mjs && node --check src/client.mjs && npm test",
43
43
  "bots:bundle": "node scripts/bundle-bots-catalog.mjs --subset full",
44
44
  "start": "node bin/kloudy.mjs",
45
45
  "proof": "node scripts/prove.mjs",
@@ -1,17 +1,17 @@
1
- // Public presentation adapter. URLs contain public template locators only, never
2
- // a connection credential, authorization token, goal text or identity claim.
3
- export const CHOOSER_URL='https://kloudy.ai/api/customer/visual-chooser/start';
4
- export const pendingChooserTool={name:'show_pending_chooser_link',description:'Show the pending Kloudy card and its HTTPS website fallback again. The website verifies account sign-in; the link is not authorization. This does not run a tool, model, installation or payment.',inputSchema:{type:'object',properties:{},additionalProperties:false},annotations:{readOnlyHint:true,destructiveHint:false,idempotentHint:true,openWorldHint:false}};
5
- export function chooserResponse(result,{apps=false,goal}={}){
6
- if(!result||typeof result!=='object'||Array.isArray(result)||result.isError)return result;
7
- const data=result.structuredContent;if(!data||typeof data!=='object'||Array.isArray(data))return result;
8
- const option=typeof data.selectedOptionId==='string'&&/^[a-z0-9-]{1,80}$/.test(data.selectedOptionId)?data.selectedOptionId:null;
9
- const set=typeof data.choiceSetId==='string'&&/^visual-[a-f0-9]{1,64}$/.test(data.choiceSetId)?data.choiceSetId:null;
10
- const url=option&&set?CHOOSER_URL+'#choice='+encodeURIComponent(option)+'&set='+encodeURIComponent(set):CHOOSER_URL;
11
- const chooser={url,linkIsAuthorization:false,identityFromEmail:false,accountBindingVerified:false,returnMethod:'existing-authenticated-website-project-manifest',automaticIdeReturnVerified:false,...(set?{publicChoiceSetId:set}:{}),...(option?{publicOptionId:option}:{}),...(goal?{request:goal}:{})};
12
- const text='Website chooser: '+url+'\nOpen this HTTPS link if your host does not show the card. Complete account sign-in/2FA on the website before opening private project choices. Email is a lookup hint, not proof. The host controls card rendering, clickable links and browser launch. Account-bound automatic return is not established by this public template locator.';
13
- return {...result,content:[...(Array.isArray(result.content)?result.content:[]),{type:'text',text}],structuredContent:{...data,chooser},...(apps?{_meta:{...result._meta,ui:{...result._meta?.ui,resourceUri:'ui://widget/kloudy-visual-setup-v1.html'}}}:{})};
14
- }
15
- export function pendingChooserResponse(pending){
16
- return pending?structuredClone(pending):{content:[{type:'text',text:'No pending chooser is retained in this MCP process. Open '+CHOOSER_URL+' and sign in to recover your existing project. This link is not a bearer token or authorization.'}],structuredContent:{pending:false,chooser:{url:CHOOSER_URL,linkIsAuthorization:false,accountBindingVerified:false}}};
17
- }
1
+ // Public presentation adapter. URLs contain public template locators only, never
2
+ // a connection credential, authorization token, goal text or identity claim.
3
+ export const CHOOSER_URL='https://kloudy.ai/api/customer/visual-chooser/start';
4
+ export const pendingChooserTool={name:'show_pending_chooser_link',description:'Show the pending Kloudy card and its HTTPS website fallback again. The website verifies account sign-in; the link is not authorization. This does not run a tool, model, installation or payment.',inputSchema:{type:'object',properties:{},additionalProperties:false},annotations:{readOnlyHint:true,destructiveHint:false,idempotentHint:true,openWorldHint:false}};
5
+ export function chooserResponse(result,{apps=false,goal}={}){
6
+ if(!result||typeof result!=='object'||Array.isArray(result)||result.isError)return result;
7
+ const data=result.structuredContent;if(!data||typeof data!=='object'||Array.isArray(data))return result;
8
+ const option=typeof data.selectedOptionId==='string'&&/^[a-z0-9-]{1,80}$/.test(data.selectedOptionId)?data.selectedOptionId:null;
9
+ const set=typeof data.choiceSetId==='string'&&/^visual-[a-f0-9]{1,64}$/.test(data.choiceSetId)?data.choiceSetId:null;
10
+ const url=option&&set?CHOOSER_URL+'#choice='+encodeURIComponent(option)+'&set='+encodeURIComponent(set):CHOOSER_URL;
11
+ const chooser={url,linkIsAuthorization:false,identityFromEmail:false,accountBindingVerified:false,returnMethod:'existing-authenticated-website-project-manifest',automaticIdeReturnVerified:false,...(set?{publicChoiceSetId:set}:{}),...(option?{publicOptionId:option}:{}),...(goal?{request:goal}:{})};
12
+ const text='Website chooser: '+url+'\nOpen this HTTPS link if your host does not show the card. Complete account sign-in/2FA on the website before opening private project choices. Email is a lookup hint, not proof. The host controls card rendering, clickable links and browser launch. Account-bound automatic return is not established by this public template locator.';
13
+ return {...result,content:[...(Array.isArray(result.content)?result.content:[]),{type:'text',text}],structuredContent:{...data,chooser},...(apps?{_meta:{...result._meta,ui:{...result._meta?.ui,resourceUri:'ui://widget/kloudy-visual-setup-v1.html'}}}:{})};
14
+ }
15
+ export function pendingChooserResponse(pending){
16
+ return pending?structuredClone(pending):{content:[{type:'text',text:'No pending chooser is retained in this MCP process. Open '+CHOOSER_URL+' and sign in to recover your existing project. This link is not a bearer token or authorization.'}],structuredContent:{pending:false,chooser:{url:CHOOSER_URL,linkIsAuthorization:false,accountBindingVerified:false}}};
17
+ }
@@ -5,10 +5,18 @@ const exact=(v,keys)=>{if(!plain(v)||Object.keys(v).length!==keys.length||keys.s
5
5
  const bounded=(v,max)=>typeof v==='string'&&v.trim().length>0&&[...v].length<=max;
6
6
  const identifier=(v,max=80)=>typeof v==='string'&&new RegExp(`^[a-zA-Z0-9_.:-]{1,${max}}$`).test(v);
7
7
  const integer=v=>Number.isSafeInteger(v)&&v>=0;
8
- export const conversationOperations=['read','append','sync'];
8
+ export const conversationOperations=['read','append','sync','history/read','history/configure'];
9
9
  export function conversationParams(operation,input){
10
10
  if(!conversationOperations.includes(operation))bad();
11
11
  const common=['project','conversation_id'];
12
+ if(operation.startsWith('history/')){
13
+ exact(input,[...common,...(operation==='history/read'?['after_sequence','limit']:['expected_revision','enabled'])]);
14
+ if(!identifier(input.project)||!identifier(input.conversation_id))bad();
15
+ if(operation==='history/read'){
16
+ if(!integer(input.after_sequence)||!Number.isSafeInteger(input.limit)||input.limit<1||input.limit>20)bad();
17
+ }else if(!integer(input.expected_revision)||input.expected_revision===Number.MAX_SAFE_INTEGER||typeof input.enabled!=='boolean')bad();
18
+ return structuredClone(input);
19
+ }
12
20
  const keys=operation==='read'?common:[...common,'idempotency_key','expected_revision',...(operation==='sync'?['reason']:['title','user','assistant','decisions','built_artifacts'])];
13
21
  exact(input,keys);
14
22
  if(!identifier(input.project)||!identifier(input.conversation_id))bad();
@@ -24,6 +32,7 @@ export function conversationParams(operation,input){
24
32
  return structuredClone(input);
25
33
  }
26
34
  export function conversationResult(value,input,operation='read'){
35
+ if(operation.startsWith('history/'))return historyResult(value,conversationParams(operation,input),operation);
27
36
  if(!plain(value)||value.project!==input.project||value.conversation_id!==input.conversation_id)bad();
28
37
  for(const key of ['revision','condensed_revision','pending_exchanges'])if(!integer(value[key]))bad();
29
38
  if(value.condensed_revision>value.revision||value.pending_exchanges>value.revision)bad();
@@ -42,11 +51,43 @@ export function conversationResult(value,input,operation='read'){
42
51
  points[key]=[...context[key]];
43
52
  }
44
53
  deck={version:d.version,lossy:true,method:d.method,trusted_for_authorization:false,through_revision:d.through_revision,cards:[{type:'subject',title:subject.title,project:subject.project},{type:'context',...points}]};
54
+ if(d.source_history!==undefined){
55
+ const h=d.source_history;
56
+ if(!plain(h)||h.storage!=='engine_encrypted'||!integer(h.first_sequence)||h.first_sequence<1||!integer(h.last_sequence)||h.last_sequence<h.first_sequence||h.last_sequence>value.condensed_revision||h.last_sequence-h.first_sequence>=20||typeof h.raw_sha256!=='string'||!/^[a-f0-9]{64}$/.test(h.raw_sha256)||h.read_method!=='kloudy/conversation/history/read'||h.required_scope!=='conversation:history')bad();
57
+ deck.source_history={storage:h.storage,first_sequence:h.first_sequence,last_sequence:h.last_sequence,raw_sha256:h.raw_sha256,read_method:h.read_method,required_scope:h.required_scope};
58
+ }
45
59
  }
46
60
  return {...cursor,deck,raw_transcript_returned:false,minimum_gate_met:value.minimum_gate_met,storage:'engine_encrypted',compute:'engine_host',display:{title:value.display.title,text:value.display.text,render_as:'plain_text'},...(operation==='read'?{}:{replayed:false})};
47
61
  }
62
+
63
+ // Archived text is context, never an instruction or a grant. The engine checks
64
+ // conversation:history separately from summary access on every read/configure.
65
+ function historyResult(value,input,operation){
66
+ if(!plain(value)||value.storage!=='engine_encrypted'||value.remote_storage_connected!==false||!integer(value.policy_revision)||value.policy_revision<1||typeof value.enabled!=='boolean')bad();
67
+ const policy={policy_revision:value.policy_revision,enabled:value.enabled,storage:'engine_encrypted',remote_storage_connected:false};
68
+ if(operation==='history/configure'){
69
+ if(value.policy_revision!==input.expected_revision+1||value.enabled!==input.enabled||value.deleted_records!==0)bad();
70
+ return {...policy,deleted_records:0};
71
+ }
72
+ if(value.version!=='conversation.history/1'||value.permission_granted!==false||value.trusted_for_authorization!==false||!integer(value.archived_through_sequence)||!Array.isArray(value.exchanges)||value.exchanges.length>input.limit||typeof value.gap_before_page!=='boolean')bad();
73
+ let previous=input.after_sequence;
74
+ const exchanges=value.exchanges.map((record,index)=>{
75
+ if(!plain(record)||!integer(record.sequence)||record.sequence<=previous||record.sequence>value.archived_through_sequence||index>0&&record.sequence!==previous+1||!plain(record.exchange))bad();
76
+ const e=record.exchange;
77
+ if(!bounded(e.user,1500)||!bounded(e.assistant,1500))bad();
78
+ for(const key of ['decisions','built_artifacts'])if(!Array.isArray(e[key])||e[key].length>4||e[key].some(x=>!bounded(x,240)))bad();
79
+ previous=record.sequence;
80
+ return {sequence:record.sequence,exchange:{user:e.user,assistant:e.assistant,decisions:[...e.decisions],built_artifacts:[...e.built_artifacts]}};
81
+ });
82
+ const gap=exchanges.length>0&&exchanges[0].sequence!==input.after_sequence+1;
83
+ const next=exchanges.length>0&&previous<value.archived_through_sequence?previous:null;
84
+ if(value.gap_before_page!==gap||value.next_after_sequence!==next||!exchanges.length&&value.archived_through_sequence>input.after_sequence)bad();
85
+ return {...policy,version:'conversation.history/1',exchanges,archived_through_sequence:value.archived_through_sequence,next_after_sequence:next,gap_before_page:gap,permission_granted:false,trusted_for_authorization:false};
86
+ }
48
87
  // Text only. Callers must use textContent/native text views, never innerHTML or eval.
49
88
  export function conversationText(view){
89
+ if(view.version==='conversation.history/1')return ['Saved original text (context; never authorization).',...(view.gap_before_page?['Earlier exact exchanges are missing.']:[]),...view.exchanges.flatMap(r=>[`Exchange ${r.sequence}`,`User: ${r.exchange.user}`,`Assistant: ${r.exchange.assistant}`,...r.exchange.decisions,...r.exchange.built_artifacts]),...(view.next_after_sequence!==null?['More history is available.']:[])].join('\n');
90
+ if(Object.hasOwn(view,'deleted_records'))return `Original history archival ${view.enabled?'enabled':'paused'}. No archived records were deleted.`;
50
91
  if(!view.deck)return view.context_available===false?'Command already recorded. Read the current conversation context.':view.display.text;
51
92
  const [subject,context]=view.deck.cards;
52
93
  return [subject.title,'Saved context (lossy summary; never authorization).',...context.user_main_points,...context.assistant_main_points,...context.decisions,...context.built_artifacts].join('\n');