appduct 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +274 -0
  3. package/bin.js +2 -0
  4. package/dist/artifact-inspect.d.ts +110 -0
  5. package/dist/artifact-inspect.d.ts.map +1 -0
  6. package/dist/artifact-inspect.js +315 -0
  7. package/dist/bin.d.ts +3 -0
  8. package/dist/bin.d.ts.map +1 -0
  9. package/dist/bin.js +4 -0
  10. package/dist/cli/command-options.d.ts +44 -0
  11. package/dist/cli/command-options.d.ts.map +1 -0
  12. package/dist/cli/command-options.js +95 -0
  13. package/dist/cli/create-cli.d.ts +2 -0
  14. package/dist/cli/create-cli.d.ts.map +1 -0
  15. package/dist/cli/create-cli.js +56 -0
  16. package/dist/cli/dispatch.d.ts +3 -0
  17. package/dist/cli/dispatch.d.ts.map +1 -0
  18. package/dist/cli/dispatch.js +275 -0
  19. package/dist/cli/open-target.d.ts +112 -0
  20. package/dist/cli/open-target.d.ts.map +1 -0
  21. package/dist/cli/open-target.js +438 -0
  22. package/dist/cli/result-types.d.ts +194 -0
  23. package/dist/cli/result-types.d.ts.map +1 -0
  24. package/dist/cli/result-types.js +6 -0
  25. package/dist/cli/runner.d.ts +35 -0
  26. package/dist/cli/runner.d.ts.map +1 -0
  27. package/dist/cli/runner.js +111 -0
  28. package/dist/cli/types.d.ts +20 -0
  29. package/dist/cli/types.d.ts.map +1 -0
  30. package/dist/cli/types.js +10 -0
  31. package/dist/cli.d.ts +4 -0
  32. package/dist/cli.d.ts.map +1 -0
  33. package/dist/cli.js +2 -0
  34. package/dist/client/app-client.d.ts +126 -0
  35. package/dist/client/app-client.d.ts.map +1 -0
  36. package/dist/client/app-client.js +223 -0
  37. package/dist/client/bootstrap.d.ts +48 -0
  38. package/dist/client/bootstrap.d.ts.map +1 -0
  39. package/dist/client/bootstrap.js +108 -0
  40. package/dist/client/connect.d.ts +17 -0
  41. package/dist/client/connect.d.ts.map +1 -0
  42. package/dist/client/connect.js +38 -0
  43. package/dist/client/errors.d.ts +26 -0
  44. package/dist/client/errors.d.ts.map +1 -0
  45. package/dist/client/errors.js +40 -0
  46. package/dist/client/index.d.ts +19 -0
  47. package/dist/client/index.d.ts.map +1 -0
  48. package/dist/client/index.js +16 -0
  49. package/dist/commands/daemon.d.ts +22 -0
  50. package/dist/commands/daemon.d.ts.map +1 -0
  51. package/dist/commands/daemon.js +128 -0
  52. package/dist/commands/doctor.d.ts +23 -0
  53. package/dist/commands/doctor.d.ts.map +1 -0
  54. package/dist/commands/doctor.js +42 -0
  55. package/dist/commands/events.d.ts +37 -0
  56. package/dist/commands/events.d.ts.map +1 -0
  57. package/dist/commands/events.js +76 -0
  58. package/dist/commands/init.d.ts +51 -0
  59. package/dist/commands/init.d.ts.map +1 -0
  60. package/dist/commands/init.js +231 -0
  61. package/dist/commands/invoke.d.ts +22 -0
  62. package/dist/commands/invoke.d.ts.map +1 -0
  63. package/dist/commands/invoke.js +54 -0
  64. package/dist/commands/keygen.d.ts +23 -0
  65. package/dist/commands/keygen.d.ts.map +1 -0
  66. package/dist/commands/keygen.js +46 -0
  67. package/dist/commands/link.d.ts +30 -0
  68. package/dist/commands/link.d.ts.map +1 -0
  69. package/dist/commands/link.js +43 -0
  70. package/dist/commands/ls.d.ts +9 -0
  71. package/dist/commands/ls.d.ts.map +1 -0
  72. package/dist/commands/ls.js +7 -0
  73. package/dist/commands/mcp.d.ts +49 -0
  74. package/dist/commands/mcp.d.ts.map +1 -0
  75. package/dist/commands/mcp.js +98 -0
  76. package/dist/commands/revoke.d.ts +12 -0
  77. package/dist/commands/revoke.d.ts.map +1 -0
  78. package/dist/commands/revoke.js +7 -0
  79. package/dist/commands/tools.d.ts +23 -0
  80. package/dist/commands/tools.d.ts.map +1 -0
  81. package/dist/commands/tools.js +52 -0
  82. package/dist/daemon/address.d.ts +30 -0
  83. package/dist/daemon/address.d.ts.map +1 -0
  84. package/dist/daemon/address.js +71 -0
  85. package/dist/daemon/audit.d.ts +107 -0
  86. package/dist/daemon/audit.d.ts.map +1 -0
  87. package/dist/daemon/audit.js +237 -0
  88. package/dist/daemon/calls.d.ts +84 -0
  89. package/dist/daemon/calls.d.ts.map +1 -0
  90. package/dist/daemon/calls.js +158 -0
  91. package/dist/daemon/config.d.ts +79 -0
  92. package/dist/daemon/config.d.ts.map +1 -0
  93. package/dist/daemon/config.js +170 -0
  94. package/dist/daemon/daemon.d.ts +43 -0
  95. package/dist/daemon/daemon.d.ts.map +1 -0
  96. package/dist/daemon/daemon.js +560 -0
  97. package/dist/daemon/event-bus.d.ts +49 -0
  98. package/dist/daemon/event-bus.d.ts.map +1 -0
  99. package/dist/daemon/event-bus.js +111 -0
  100. package/dist/daemon/links.d.ts +54 -0
  101. package/dist/daemon/links.d.ts.map +1 -0
  102. package/dist/daemon/links.js +118 -0
  103. package/dist/daemon/listener.d.ts +34 -0
  104. package/dist/daemon/listener.d.ts.map +1 -0
  105. package/dist/daemon/listener.js +132 -0
  106. package/dist/daemon/log-rotation.d.ts +82 -0
  107. package/dist/daemon/log-rotation.d.ts.map +1 -0
  108. package/dist/daemon/log-rotation.js +136 -0
  109. package/dist/daemon/pidfile.d.ts +38 -0
  110. package/dist/daemon/pidfile.d.ts.map +1 -0
  111. package/dist/daemon/pidfile.js +90 -0
  112. package/dist/daemon/policy.d.ts +22 -0
  113. package/dist/daemon/policy.d.ts.map +1 -0
  114. package/dist/daemon/policy.js +24 -0
  115. package/dist/daemon/registry.d.ts +19 -0
  116. package/dist/daemon/registry.d.ts.map +1 -0
  117. package/dist/daemon/registry.js +31 -0
  118. package/dist/daemon/rpc-server.d.ts +54 -0
  119. package/dist/daemon/rpc-server.d.ts.map +1 -0
  120. package/dist/daemon/rpc-server.js +203 -0
  121. package/dist/daemon/sessions.d.ts +86 -0
  122. package/dist/daemon/sessions.d.ts.map +1 -0
  123. package/dist/daemon/sessions.js +429 -0
  124. package/dist/daemon/socket-probe.d.ts +16 -0
  125. package/dist/daemon/socket-probe.d.ts.map +1 -0
  126. package/dist/daemon/socket-probe.js +28 -0
  127. package/dist/daemon/state-dir.d.ts +31 -0
  128. package/dist/daemon/state-dir.d.ts.map +1 -0
  129. package/dist/daemon/state-dir.js +43 -0
  130. package/dist/daemon/timers.d.ts +16 -0
  131. package/dist/daemon/timers.d.ts.map +1 -0
  132. package/dist/daemon/timers.js +12 -0
  133. package/dist/daemon/tls.d.ts +58 -0
  134. package/dist/daemon/tls.d.ts.map +1 -0
  135. package/dist/daemon/tls.js +79 -0
  136. package/dist/errors.d.ts +28 -0
  137. package/dist/errors.d.ts.map +1 -0
  138. package/dist/errors.js +162 -0
  139. package/dist/host-certificate.d.ts +8 -0
  140. package/dist/host-certificate.d.ts.map +1 -0
  141. package/dist/host-certificate.js +111 -0
  142. package/dist/index.d.ts +4 -0
  143. package/dist/index.d.ts.map +1 -0
  144. package/dist/index.js +3 -0
  145. package/dist/key-material.d.ts +14 -0
  146. package/dist/key-material.d.ts.map +1 -0
  147. package/dist/key-material.js +42 -0
  148. package/dist/link.d.ts +76 -0
  149. package/dist/link.d.ts.map +1 -0
  150. package/dist/link.js +117 -0
  151. package/dist/mcp/connect-tool.d.ts +115 -0
  152. package/dist/mcp/connect-tool.d.ts.map +1 -0
  153. package/dist/mcp/connect-tool.js +396 -0
  154. package/dist/mcp/daemon-tools.d.ts +13 -0
  155. package/dist/mcp/daemon-tools.d.ts.map +1 -0
  156. package/dist/mcp/daemon-tools.js +29 -0
  157. package/dist/mcp/events-tool.d.ts +111 -0
  158. package/dist/mcp/events-tool.d.ts.map +1 -0
  159. package/dist/mcp/events-tool.js +322 -0
  160. package/dist/mcp/server.d.ts +58 -0
  161. package/dist/mcp/server.d.ts.map +1 -0
  162. package/dist/mcp/server.js +538 -0
  163. package/dist/mcp/tool-mapping.d.ts +58 -0
  164. package/dist/mcp/tool-mapping.d.ts.map +1 -0
  165. package/dist/mcp/tool-mapping.js +143 -0
  166. package/dist/mcp/tool-namespace.d.ts +40 -0
  167. package/dist/mcp/tool-namespace.d.ts.map +1 -0
  168. package/dist/mcp/tool-namespace.js +65 -0
  169. package/dist/native-scheme.d.ts +59 -0
  170. package/dist/native-scheme.d.ts.map +1 -0
  171. package/dist/native-scheme.js +465 -0
  172. package/dist/output.d.ts +28 -0
  173. package/dist/output.d.ts.map +1 -0
  174. package/dist/output.js +342 -0
  175. package/dist/package-version.d.ts +30 -0
  176. package/dist/package-version.d.ts.map +1 -0
  177. package/dist/package-version.js +47 -0
  178. package/dist/qr-terminal.d.ts +13 -0
  179. package/dist/qr-terminal.d.ts.map +1 -0
  180. package/dist/qr-terminal.js +45 -0
  181. package/dist/rpc/client.d.ts +137 -0
  182. package/dist/rpc/client.d.ts.map +1 -0
  183. package/dist/rpc/client.js +819 -0
  184. package/dist/scheme.d.ts +181 -0
  185. package/dist/scheme.d.ts.map +1 -0
  186. package/dist/scheme.js +321 -0
  187. package/dist/spki-pin.d.ts +3 -0
  188. package/dist/spki-pin.d.ts.map +1 -0
  189. package/dist/spki-pin.js +14 -0
  190. package/package.json +65 -0
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Maps a `NamespacedTool` (daemon `ToolDescriptor` + resolved external name) onto the MCP SDK's
3
+ * `Tool` wire shape (ARCHITECTURE.md §9/§7): `description` maps directly and `annotations` map
4
+ * verbatim, while `input_schema`/`output_schema` are emitted only if the SDK's own `ToolSchema`
5
+ * accepts them (issue #26) — a rejected `input_schema` is replaced with an empty, permissive
6
+ * object schema and a rejected `output_schema` is dropped entirely. Either way the tool stays
7
+ * listed and callable: one app-side schema MCP cannot represent must never take down the whole
8
+ * `tools/list`.
9
+ *
10
+ * The gate is the SDK schema itself rather than a hand-rolled `type === "object"` check, because
11
+ * MCP constrains more than the root type — `properties` must be a record of *object* subschemas
12
+ * (the JSON Schema shorthand `properties: { a: true }` is rejected) and `required` must be an
13
+ * array. Any of those makes a client reject the entire `tools/list` result, so the only safe
14
+ * predicate is the one the client will actually apply.
15
+ *
16
+ * A tool whose effective policy is `"prompt"` (ARCHITECTURE.md §12) gets
17
+ * `_meta["anthropic/requiresUserInteraction"] = true` — but only when the caller says to
18
+ * (`emitRequiresUserInteractionFlag`), which `mcp/server.ts` decides is true only when the
19
+ * connected client is known to enforce the flag (issue #14) *and* it hasn't already preferred the
20
+ * elicitation channel instead (issue #10). Emitting the flag for a client that ignores it would
21
+ * create a false sense of security; emitting it *and* using elicitation for the same tool would
22
+ * arm two consent prompts for one call — so this function never decides that itself, only renders
23
+ * the decision it's handed.
24
+ */
25
+ import { ToolSchema } from "@modelcontextprotocol/sdk/types.js";
26
+ /** MCP requires `inputSchema.type === "object"`; an app that registered a tool without a schema
27
+ * gets the most permissive possible one rather than an MCP-invalid `{}`. */
28
+ const EMPTY_OBJECT_SCHEMA = { type: "object", additionalProperties: true };
29
+ /**
30
+ * Why the SDK's `ToolSchema` would reject `schema` in this slot, or `undefined` when it accepts
31
+ * it. The probe wraps the schema in an otherwise-minimal valid `Tool` so the only thing under test
32
+ * is the schema; the returned reason is the first issue, with the leading `inputSchema`/
33
+ * `outputSchema` path segment stripped so the message reads from the app author's point of view.
34
+ */
35
+ const mcpSchemaRejection = (schema, slot) => {
36
+ const probe = slot === "input"
37
+ ? { name: "probe", inputSchema: schema }
38
+ : { name: "probe", inputSchema: EMPTY_OBJECT_SCHEMA, outputSchema: schema };
39
+ const parsed = ToolSchema.safeParse(probe);
40
+ if (parsed.success) {
41
+ return undefined;
42
+ }
43
+ const issue = parsed.error.issues[0];
44
+ if (!issue) {
45
+ return "it does not match MCP's tool schema";
46
+ }
47
+ const path = issue.path.slice(1).join(".");
48
+ return path.length > 0 ? `${path}: ${issue.message}` : issue.message;
49
+ };
50
+ /**
51
+ * Whether `tools/list` will carry an `outputSchema` for this descriptor. `server.ts`'s success
52
+ * path shares this predicate so the "did we advertise a schema?" and "must this result carry
53
+ * `structuredContent`?" decisions can never disagree — a client demands the latter exactly when
54
+ * it saw the former.
55
+ */
56
+ export const emitsMcpOutputSchema = (schema) => {
57
+ return schema !== undefined && mcpSchemaRejection(schema, "output") === undefined;
58
+ };
59
+ /** stderr only — stdout carries the MCP transport's protocol frames (ARCHITECTURE.md §9). */
60
+ const defaultWarn = (message) => {
61
+ console.error(`appduct mcp: ${message}`);
62
+ };
63
+ /**
64
+ * Upper bound on remembered notices. A key embeds the offending schema, and an app is free to
65
+ * build a schema from live data (a `z.enum` over rows it just fetched), so an unbounded set would
66
+ * grow with every distinct broken schema for the life of the process. Past the cap the oldest key
67
+ * is evicted, which at worst re-prints a notice the operator has already seen.
68
+ */
69
+ const MAX_REMEMBERED_NOTICES = 256;
70
+ /**
71
+ * Builds a mapper with its own degradation-notice dedup, so nothing here is module state and no
72
+ * reset hook has to be exported for tests: a server owns one mapper for its lifetime, and a test
73
+ * owns one per case. `warn` defaults to stderr and exists so a test can capture notices without
74
+ * spying on the console.
75
+ *
76
+ * Notices dedupe on session + tool name + the offending schema, because `tools/list` is
77
+ * re-answered on every client request and on every `notifications/tools/list_changed` refresh. Two
78
+ * devices exposing the same broken tool each get their own notice, and re-registering a tool with
79
+ * a *differently* broken schema warns again; only the same problem on the same tool of the same
80
+ * session stays quiet. The session's `selector` is the key rather than the namespaced `mcpName` so
81
+ * the single↔multi session flip, which rewrites every `mcpName`, does not re-warn about tools
82
+ * nothing changed about.
83
+ */
84
+ export const createMcpToolMapper = (warn = defaultWarn) => {
85
+ const warned = new Set();
86
+ const warnOnce = (tool, slot, schema, message) => {
87
+ const key = `${tool.selector}\u0000${tool.descriptor.name}\u0000${slot}\u0000${JSON.stringify(schema)}`;
88
+ if (warned.has(key)) {
89
+ return;
90
+ }
91
+ // A `Set` iterates in insertion order, so the first key is the oldest.
92
+ if (warned.size >= MAX_REMEMBERED_NOTICES) {
93
+ const oldest = warned.values().next();
94
+ if (!oldest.done) {
95
+ warned.delete(oldest.value);
96
+ }
97
+ }
98
+ warned.add(key);
99
+ warn(message);
100
+ };
101
+ const mapInputSchema = (tool) => {
102
+ const schema = tool.descriptor.input_schema;
103
+ if (schema === undefined) {
104
+ return EMPTY_OBJECT_SCHEMA;
105
+ }
106
+ const rejection = mcpSchemaRejection(schema, "input");
107
+ if (rejection === undefined) {
108
+ return schema;
109
+ }
110
+ warnOnce(tool, "input", schema, `Tool "${tool.mcpName}" declares an input schema MCP cannot accept (${rejection}); MCP tool ` +
111
+ "arguments are always an object, so the schema was replaced with a permissive empty object " +
112
+ "schema and agents cannot see the tool's real arguments. Wrap the input in an object schema.");
113
+ return EMPTY_OBJECT_SCHEMA;
114
+ };
115
+ const mapOutputSchema = (tool) => {
116
+ const schema = tool.descriptor.output_schema;
117
+ if (schema === undefined) {
118
+ return undefined;
119
+ }
120
+ const rejection = mcpSchemaRejection(schema, "output");
121
+ if (rejection === undefined) {
122
+ return schema;
123
+ }
124
+ warnOnce(tool, "output", schema, `Tool "${tool.mcpName}" declares an output schema MCP cannot accept (${rejection}); it was ` +
125
+ "dropped from tools/list, so agents get the result without a schema to validate it against. " +
126
+ "Wrap the output in an object schema to give agents a described, structured result.");
127
+ return undefined;
128
+ };
129
+ return (tool, emitRequiresUserInteractionFlag) => {
130
+ const { descriptor } = tool;
131
+ const requiresUserInteraction = tool.policy === "prompt" && emitRequiresUserInteractionFlag;
132
+ const outputSchema = mapOutputSchema(tool);
133
+ return {
134
+ name: tool.mcpName,
135
+ description: descriptor.description,
136
+ inputSchema: mapInputSchema(tool),
137
+ ...(outputSchema ? { outputSchema } : {}),
138
+ ...(descriptor.annotations ? { annotations: { ...descriptor.annotations } } : {}),
139
+ // Must be the JSON boolean `true` literal — any other value is ignored by the client.
140
+ ...(requiresUserInteraction ? { _meta: { "anthropic/requiresUserInteraction": true } } : {}),
141
+ };
142
+ };
143
+ };
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Builds the effective MCP tool list from live daemon sessions and their tool registries
3
+ * (ARCHITECTURE.md §9): exactly one live session exposes tools under their
4
+ * own names; two or more namespace every tool as `<alias>__<name>` so names never collide across
5
+ * devices.
6
+ *
7
+ * The `__` separator is unambiguous to split back apart even though a tool's own name
8
+ * (`[a-zA-Z0-9_-]{1,64}`, `daemon/registry.ts`) may itself contain underscores: aliases
9
+ * (`daemon/sessions.ts`'s `slugifyDeviceModel`/`dedupeAlias`) only ever contain lowercase
10
+ * alphanumerics and hyphens, so the alias half of a namespaced name never itself contains `__` —
11
+ * the first occurrence of `__` in a namespaced name is always the alias/tool-name boundary.
12
+ */
13
+ import type { EffectivePolicyDecision, SessionSummary, ToolDescriptor, ToolsListEntry } from "@appduct/shared";
14
+ export declare const TOOL_NAMESPACE_SEPARATOR = "__";
15
+ export type NamespacedTool = {
16
+ /** The name exposed to MCP clients: `descriptor.name` verbatim when exactly one session is live,
17
+ * else `<alias>__<name>`. */
18
+ mcpName: string;
19
+ /** The session alias `tools.call`'s `selector` should target for this tool. */
20
+ selector: string;
21
+ descriptor: ToolDescriptor;
22
+ /** The effective policy decision for this tool right now (ARCHITECTURE.md §12), as resolved by
23
+ * the daemon's `tools.list` — carried through so the MCP server can emit
24
+ * `_meta["anthropic/requiresUserInteraction"]` and set `consent` on `tools.call` without a
25
+ * second round trip (issue #14). */
26
+ policy: EffectivePolicyDecision;
27
+ };
28
+ /** Builds the namespaced (or not) tool list for the current set of live sessions. `toolsByAlias`
29
+ * is looked up by `session.alias` (not `sessionId`) to match `tools.list`'s `selector` semantics. */
30
+ export declare const buildNamespacedTools: (sessions: readonly SessionSummary[], toolsByAlias: ReadonlyMap<string, readonly ToolsListEntry[]>) => NamespacedTool[];
31
+ export declare const findNamespacedTool: (tools: readonly NamespacedTool[], mcpName: string) => NamespacedTool | undefined;
32
+ /** A stable, order-independent key used to detect whether the effective tool list actually
33
+ * changed (name, target session, and the descriptor fields a client can see) — used to decide
34
+ * whether to fire `notifications/tools/list_changed` rather than on every qualifying daemon event.
35
+ *
36
+ * `timeout_ms` is deliberately excluded: it never reaches the MCP `Tool` JSON (`tool-mapping.ts`),
37
+ * so an app re-registering a tool with only a different deadline changes nothing a client could
38
+ * observe, and telling it the list changed would just make it re-fetch an identical list. */
39
+ export declare const namespacedToolsSnapshotKey: (tools: readonly NamespacedTool[]) => string;
40
+ //# sourceMappingURL=tool-namespace.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-namespace.d.ts","sourceRoot":"","sources":["../../src/mcp/tool-namespace.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,uBAAuB,EAAE,cAAc,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAE/G,eAAO,MAAM,wBAAwB,OAAO,CAAC;AAE7C,MAAM,MAAM,cAAc,GAAG;IAC3B;iCAC6B;IAC7B,OAAO,EAAE,MAAM,CAAC;IAChB,+EAA+E;IAC/E,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,cAAc,CAAC;IAC3B;;;wCAGoC;IACpC,MAAM,EAAE,uBAAuB,CAAC;CACjC,CAAC;AAEF;qGACqG;AACrG,eAAO,MAAM,oBAAoB,GAC/B,UAAU,SAAS,cAAc,EAAE,EACnC,cAAc,WAAW,CAAC,MAAM,EAAE,SAAS,cAAc,EAAE,CAAC,KAC3D,cAAc,EA4BhB,CAAC;AAEF,eAAO,MAAM,kBAAkB,GAC7B,OAAO,SAAS,cAAc,EAAE,EAChC,SAAS,MAAM,KACd,cAAc,GAAG,SAEnB,CAAC;AAEF;;;;;;6FAM6F;AAC7F,eAAO,MAAM,0BAA0B,GAAI,OAAO,SAAS,cAAc,EAAE,KAAG,MAe7E,CAAC"}
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Builds the effective MCP tool list from live daemon sessions and their tool registries
3
+ * (ARCHITECTURE.md §9): exactly one live session exposes tools under their
4
+ * own names; two or more namespace every tool as `<alias>__<name>` so names never collide across
5
+ * devices.
6
+ *
7
+ * The `__` separator is unambiguous to split back apart even though a tool's own name
8
+ * (`[a-zA-Z0-9_-]{1,64}`, `daemon/registry.ts`) may itself contain underscores: aliases
9
+ * (`daemon/sessions.ts`'s `slugifyDeviceModel`/`dedupeAlias`) only ever contain lowercase
10
+ * alphanumerics and hyphens, so the alias half of a namespaced name never itself contains `__` —
11
+ * the first occurrence of `__` in a namespaced name is always the alias/tool-name boundary.
12
+ */
13
+ export const TOOL_NAMESPACE_SEPARATOR = "__";
14
+ /** Builds the namespaced (or not) tool list for the current set of live sessions. `toolsByAlias`
15
+ * is looked up by `session.alias` (not `sessionId`) to match `tools.list`'s `selector` semantics. */
16
+ export const buildNamespacedTools = (sessions, toolsByAlias) => {
17
+ const namespaced = sessions.length > 1;
18
+ const tools = [];
19
+ for (const session of sessions) {
20
+ for (const entry of toolsByAlias.get(session.alias) ?? []) {
21
+ // Explicit pick (not a `{ policy, ...descriptor }` rest-spread) so a future non-descriptor
22
+ // field added to `ToolsListEntry` can't silently leak into `descriptor` and from there into
23
+ // the MCP tool surface and the snapshot key below.
24
+ const descriptor = {
25
+ name: entry.name,
26
+ description: entry.description,
27
+ input_schema: entry.input_schema,
28
+ output_schema: entry.output_schema,
29
+ annotations: entry.annotations,
30
+ timeout_ms: entry.timeout_ms,
31
+ };
32
+ tools.push({
33
+ mcpName: namespaced ? `${session.alias}${TOOL_NAMESPACE_SEPARATOR}${descriptor.name}` : descriptor.name,
34
+ selector: session.alias,
35
+ descriptor,
36
+ policy: entry.policy,
37
+ });
38
+ }
39
+ }
40
+ return tools;
41
+ };
42
+ export const findNamespacedTool = (tools, mcpName) => {
43
+ return tools.find((tool) => tool.mcpName === mcpName);
44
+ };
45
+ /** A stable, order-independent key used to detect whether the effective tool list actually
46
+ * changed (name, target session, and the descriptor fields a client can see) — used to decide
47
+ * whether to fire `notifications/tools/list_changed` rather than on every qualifying daemon event.
48
+ *
49
+ * `timeout_ms` is deliberately excluded: it never reaches the MCP `Tool` JSON (`tool-mapping.ts`),
50
+ * so an app re-registering a tool with only a different deadline changes nothing a client could
51
+ * observe, and telling it the list changed would just make it re-fetch an identical list. */
52
+ export const namespacedToolsSnapshotKey = (tools) => {
53
+ const sorted = tools
54
+ .map((tool) => {
55
+ const { timeout_ms: _timeoutMs, ...clientVisibleDescriptor } = tool.descriptor;
56
+ return {
57
+ name: tool.mcpName,
58
+ selector: tool.selector,
59
+ descriptor: clientVisibleDescriptor,
60
+ policy: tool.policy,
61
+ };
62
+ })
63
+ .sort((a, b) => a.name.localeCompare(b.name));
64
+ return JSON.stringify(sorted);
65
+ };
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Static-file scheme discovery for plain iOS (Xcode) and Android (Gradle) apps
3
+ * (docs/tasks/20-cli-native-scheme-discovery.md, phase 3 of issue #48).
4
+ *
5
+ * `scheme.ts`'s `resolveScheme` treats this module as one more thing to try after `<cwd>/app.json`
6
+ * — it never walks up (an app root is where these commands run, same rule as `app.json`), and,
7
+ * like every other source in that file, **nothing here executes project code**: no `plutil`, no
8
+ * `xcodebuild`, no Gradle evaluation. Every probe is a plain read of a well-known project file,
9
+ * parsed defensively:
10
+ *
11
+ * - Android: `app/build.gradle.kts` / `app/build.gradle` for a `manifestPlaceholders["appductScheme"]`
12
+ * (or `.appductScheme =`) assignment, then `app/src/main/AndroidManifest.xml` for the first
13
+ * `<data android:scheme>` inside an intent filter that also declares
14
+ * `android.intent.action.VIEW`.
15
+ * - iOS: any `Info.plist` up to two levels below the app root (excluding `Pods`, `build`,
16
+ * `node_modules` and `DerivedData` — vendored/build-output trees that are not the project's own
17
+ * config, and can be enormous) for the first `CFBundleURLSchemes` entry, then `project.yml`
18
+ * (xcodegen) for the same key under `info.properties.CFBundleURLTypes`.
19
+ *
20
+ * A binary-encoded `Info.plist` (Xcode can write either encoding) is reported as unreadable rather
21
+ * than decoded — `plutil -convert xml1` would work, but that is exactly the "run a tool against
22
+ * project state" this module exists to avoid, so a binary plist just falls through to `--scheme`
23
+ * like any other file this can't make sense of.
24
+ *
25
+ * **Disagreement is not guessed away.** If more than one probe below resolves to a *different*
26
+ * scheme (e.g. a monorepo whose `android/` and `ios/` declare different values), discovery throws
27
+ * a usage error naming every conflicting source instead of picking one — silently choosing would
28
+ * mean `appduct link` opens the wrong app on whichever platform lost.
29
+ */
30
+ export type NativeSchemeSource = "android-gradle" | "android-manifest" | "ios-info-plist" | "ios-project-yml";
31
+ export type NativeSchemeProbeResult = {
32
+ source: NativeSchemeSource;
33
+ scheme: string;
34
+ /** Human-readable path (plus what was read from it), for disagreement errors and `init`'s hint. */
35
+ origin: string;
36
+ };
37
+ export type NativeSchemeDiscovery = {
38
+ /** `undefined` when no probe resolved a scheme. */
39
+ result?: NativeSchemeProbeResult;
40
+ /** Every probe's human-readable description, in order, for {@link describeMissingScheme}-style
41
+ * reporting — always as many entries as there are probes, whether or not any of them hit. */
42
+ tried: string[];
43
+ };
44
+ /**
45
+ * Runs every static-file probe against `root` (never walking up — same rule as `app.json`) and
46
+ * decides the result:
47
+ *
48
+ * - No probe resolves a scheme: `result` is `undefined`.
49
+ * - Every probe that resolves one agrees: that scheme wins, attributed to whichever probe ran
50
+ * first (the order in {@link PROBES}).
51
+ * - Two probes resolve *different* schemes: throws a usage error naming both sources and their
52
+ * values, since guessing which one is right would silently target the wrong app on one
53
+ * platform.
54
+ *
55
+ * `tried` always has one entry per probe, hit or miss, so a "no scheme anywhere" error can list
56
+ * every location this looked without the caller needing to know which probes exist.
57
+ */
58
+ export declare const discoverNativeScheme: (root: string) => Promise<NativeSchemeDiscovery>;
59
+ //# sourceMappingURL=native-scheme.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"native-scheme.d.ts","sourceRoot":"","sources":["../src/native-scheme.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAQH,MAAM,MAAM,kBAAkB,GAC1B,gBAAgB,GAChB,kBAAkB,GAClB,gBAAgB,GAChB,iBAAiB,CAAC;AAEtB,MAAM,MAAM,uBAAuB,GAAG;IACpC,MAAM,EAAE,kBAAkB,CAAC;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,mGAAmG;IACnG,MAAM,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,mDAAmD;IACnD,MAAM,CAAC,EAAE,uBAAuB,CAAC;IACjC;iGAC6F;IAC7F,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB,CAAC;AAihBF;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,oBAAoB,GAAU,MAAM,MAAM,KAAG,OAAO,CAAC,qBAAqB,CA4BtF,CAAC"}