frely-cli 0.23.8 → 0.23.10

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 (116) hide show
  1. package/README.md +50 -20
  2. package/README.zh-CN.md +47 -19
  3. package/dist/agent/local/approvals.d.ts +24 -0
  4. package/dist/agent/local/approvals.js +79 -0
  5. package/dist/agent/local/approvals.js.map +1 -0
  6. package/dist/agent/local/command.d.ts +10 -0
  7. package/dist/agent/local/command.js +292 -0
  8. package/dist/agent/local/command.js.map +1 -0
  9. package/dist/agent/local/confine.d.ts +20 -0
  10. package/dist/agent/local/confine.js +52 -0
  11. package/dist/agent/local/confine.js.map +1 -0
  12. package/dist/agent/local/manifest.d.ts +17 -0
  13. package/dist/agent/local/manifest.js +55 -0
  14. package/dist/agent/local/manifest.js.map +1 -0
  15. package/dist/agent/local/model-proxy.d.ts +18 -0
  16. package/dist/agent/local/model-proxy.js +111 -0
  17. package/dist/agent/local/model-proxy.js.map +1 -0
  18. package/dist/agent/local/source.d.ts +28 -0
  19. package/dist/agent/local/source.js +64 -0
  20. package/dist/agent/local/source.js.map +1 -0
  21. package/dist/agent/local/store.d.ts +45 -0
  22. package/dist/agent/local/store.js +78 -0
  23. package/dist/agent/local/store.js.map +1 -0
  24. package/dist/agent/local/supervisor.d.ts +9 -0
  25. package/dist/agent/local/supervisor.js +62 -0
  26. package/dist/agent/local/supervisor.js.map +1 -0
  27. package/dist/agent-help.d.ts +294 -18
  28. package/dist/agent-help.js +56 -7
  29. package/dist/agent-help.js.map +1 -1
  30. package/dist/auth.d.ts +5 -0
  31. package/dist/auth.js +36 -0
  32. package/dist/auth.js.map +1 -1
  33. package/dist/cloud.d.ts +2 -0
  34. package/dist/cloud.js +38 -0
  35. package/dist/cloud.js.map +1 -1
  36. package/dist/controller/args.d.ts +25 -0
  37. package/dist/controller/args.js +61 -0
  38. package/dist/controller/args.js.map +1 -0
  39. package/dist/controller/client.d.ts +52 -0
  40. package/dist/controller/client.js +124 -0
  41. package/dist/controller/client.js.map +1 -0
  42. package/dist/controller/commands.d.ts +16 -0
  43. package/dist/controller/commands.js +411 -0
  44. package/dist/controller/commands.js.map +1 -0
  45. package/dist/controller/dpop.d.ts +24 -0
  46. package/dist/controller/dpop.js +26 -0
  47. package/dist/controller/dpop.js.map +1 -0
  48. package/dist/controller/session.d.ts +53 -0
  49. package/dist/controller/session.js +211 -0
  50. package/dist/controller/session.js.map +1 -0
  51. package/dist/device/protocol.d.ts +1 -1
  52. package/dist/device/protocol.js +1 -1
  53. package/dist/device/protocol.js.map +1 -1
  54. package/dist/device/relay-client.d.ts +14 -0
  55. package/dist/device/relay-client.js +34 -14
  56. package/dist/device/relay-client.js.map +1 -1
  57. package/dist/index.js +79 -8
  58. package/dist/index.js.map +1 -1
  59. package/dist/provider/control.d.ts +10 -0
  60. package/dist/provider/control.js +19 -0
  61. package/dist/provider/control.js.map +1 -1
  62. package/dist/provider/share.js +4 -1
  63. package/dist/provider/share.js.map +1 -1
  64. package/dist/runtime/sandbox.d.ts +20 -0
  65. package/dist/runtime/sandbox.js +39 -1
  66. package/dist/runtime/sandbox.js.map +1 -1
  67. package/dist/share/allowance.d.ts +8 -0
  68. package/dist/share/allowance.js +22 -0
  69. package/dist/share/allowance.js.map +1 -0
  70. package/dist/share/control.d.ts +18 -0
  71. package/dist/share/control.js +53 -0
  72. package/dist/share/control.js.map +1 -0
  73. package/dist/share/frpc-config.d.ts +24 -0
  74. package/dist/share/frpc-config.js +63 -0
  75. package/dist/share/frpc-config.js.map +1 -0
  76. package/dist/share/frpc.d.ts +30 -0
  77. package/dist/share/frpc.js +108 -0
  78. package/dist/share/frpc.js.map +1 -0
  79. package/dist/share/gateway.d.ts +26 -0
  80. package/dist/share/gateway.js +100 -0
  81. package/dist/share/gateway.js.map +1 -0
  82. package/dist/share/run.d.ts +43 -0
  83. package/dist/share/run.js +163 -0
  84. package/dist/share/run.js.map +1 -0
  85. package/dist/share/service-config.d.ts +39 -0
  86. package/dist/share/service-config.js +85 -0
  87. package/dist/share/service-config.js.map +1 -0
  88. package/dist/share/service-control.d.ts +15 -0
  89. package/dist/share/service-control.js +31 -0
  90. package/dist/share/service-control.js.map +1 -0
  91. package/dist/share/service-run.d.ts +55 -0
  92. package/dist/share/service-run.js +285 -0
  93. package/dist/share/service-run.js.map +1 -0
  94. package/dist/share/service-state.d.ts +16 -0
  95. package/dist/share/service-state.js +66 -0
  96. package/dist/share/service-state.js.map +1 -0
  97. package/dist/skill/access.d.ts +2 -2
  98. package/dist/skill/access.js +4 -2
  99. package/dist/skill/access.js.map +1 -1
  100. package/dist/skill/cloud-item.d.ts +2 -2
  101. package/dist/skill/cloud-item.js.map +1 -1
  102. package/dist/skill/entry.d.ts +1 -1
  103. package/dist/skill/entry.js +1 -1
  104. package/dist/skill/remote-mcp.d.ts +3 -0
  105. package/dist/skill/remote-mcp.js +7 -1
  106. package/dist/skill/remote-mcp.js.map +1 -1
  107. package/dist/skill/router.d.ts +6 -2
  108. package/dist/skill/router.js +4 -4
  109. package/dist/skill/router.js.map +1 -1
  110. package/dist/skill/sealed-run.js +5 -3
  111. package/dist/skill/sealed-run.js.map +1 -1
  112. package/dist/uninstall.d.ts +4 -0
  113. package/dist/uninstall.js +14 -0
  114. package/dist/uninstall.js.map +1 -1
  115. package/package.json +3 -2
  116. package/skills/frely-cli/SKILL.md +8 -0
@@ -0,0 +1,52 @@
1
+ import { controllerLogin, type ControllerSession } from "./session.js";
2
+ export type ToolResult = {
3
+ isError?: boolean;
4
+ content?: unknown;
5
+ structuredContent?: unknown;
6
+ [key: string]: unknown;
7
+ };
8
+ /**
9
+ * `fetch` for the FrelyMCP transport: every request carries the DPoP access token and a fresh proof for that request. A refused token is
10
+ * renewed once (another process may have renewed it already) and the request sent again; a request is only ever replayed when the server
11
+ * refused it before running anything.
12
+ */
13
+ export declare function dpopFetch(initial: ControllerSession, options?: {
14
+ refresh?: (session: ControllerSession) => Promise<ControllerSession>;
15
+ now?: () => number;
16
+ }): {
17
+ fetch: typeof fetch;
18
+ session(): ControllerSession;
19
+ };
20
+ export declare const NOT_AUTHORIZED = "This computer is not authorized to control your devices yet. Run frely controller login in a terminal, approve the request in your browser, then run this command again.";
21
+ export interface Controller {
22
+ /** The server's own instructions (how to use the tools), printed first by `frely tools`. */
23
+ instructions(): string | undefined;
24
+ tools(): Promise<Array<{
25
+ name: string;
26
+ description?: string | undefined;
27
+ inputSchema?: unknown;
28
+ annotations?: {
29
+ readOnlyHint?: boolean | undefined;
30
+ } | undefined;
31
+ }>>;
32
+ call(name: string, args: Record<string, unknown>): Promise<ToolResult>;
33
+ close(): Promise<void>;
34
+ }
35
+ /** Opens a controller connection, signing the computer in first when there is no session and a person can approve it. */
36
+ export declare function openController(options?: {
37
+ interactive: boolean;
38
+ login?: typeof controllerLogin;
39
+ }): Promise<Controller>;
40
+ /** The tool's text parts joined, as a person or model would read them. */
41
+ export declare function resultText(result: ToolResult): string;
42
+ /** The result as one object: its structured content, else its text when that is a JSON object. */
43
+ export declare function resultData(result: ToolResult): Record<string, unknown> | null;
44
+ export interface Outcome {
45
+ exitCode: number; /** Why the command did not simply succeed: what to do next, for the output's end. */
46
+ note?: string;
47
+ }
48
+ /**
49
+ * What a tool result means for the exit code. A pending approval (3) comes with where to approve and the command that waits for it; an
50
+ * offline device (5), a quota (6) and any other tool error (2) keep the server's own words in the output.
51
+ */
52
+ export declare function outcomeOf(result: ToolResult): Outcome;
@@ -0,0 +1,124 @@
1
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
3
+ import { VERSION } from "../version.js";
4
+ import { createDpopProof, dpopKeyFromPem } from "./dpop.js";
5
+ import { controllerLogin, ControllerError, EXIT, readSession, refreshSession } from "./session.js";
6
+ /**
7
+ * `fetch` for the FrelyMCP transport: every request carries the DPoP access token and a fresh proof for that request. A refused token is
8
+ * renewed once (another process may have renewed it already) and the request sent again; a request is only ever replayed when the server
9
+ * refused it before running anything.
10
+ */
11
+ export function dpopFetch(initial, options = {}) {
12
+ let session = initial;
13
+ const refresh = options.refresh ?? refreshSession;
14
+ const now = options.now ?? Date.now;
15
+ const send = (input, init) => {
16
+ const url = input instanceof Request ? input.url : String(input);
17
+ const method = init?.method ?? (input instanceof Request ? input.method : "GET");
18
+ const headers = new Headers(init?.headers);
19
+ headers.set("authorization", `DPoP ${session.accessToken}`);
20
+ headers.set("dpop", createDpopProof(dpopKeyFromPem(session.privateKeyPem), { method, url, accessToken: session.accessToken }));
21
+ return fetch(url, { ...init, headers, redirect: "error" });
22
+ };
23
+ return {
24
+ session: () => session,
25
+ fetch: async (input, init) => {
26
+ if (session.expiresAt - 30_000 <= now())
27
+ session = await refresh(session);
28
+ let response = await send(input, init);
29
+ if (response.status === 401) {
30
+ await response.body?.cancel().catch(() => undefined);
31
+ session = await refresh(session);
32
+ response = await send(input, init);
33
+ if (response.status === 401)
34
+ throw new ControllerError("The server did not accept this computer's controller authorization. Run the command again to approve it in your browser.", EXIT.auth);
35
+ }
36
+ return response;
37
+ },
38
+ };
39
+ }
40
+ export const NOT_AUTHORIZED = "This computer is not authorized to control your devices yet. Run frely controller login in a terminal, approve the request in your browser, then run this command again.";
41
+ /** Opens a controller connection, signing the computer in first when there is no session and a person can approve it. */
42
+ export async function openController(options = { interactive: false }) {
43
+ let session = await readSession();
44
+ if (!session) {
45
+ if (!options.interactive)
46
+ throw new ControllerError(NOT_AUTHORIZED, EXIT.auth);
47
+ session = await (options.login ?? controllerLogin)();
48
+ }
49
+ const signed = dpopFetch(session);
50
+ const transport = new StreamableHTTPClientTransport(new URL(session.mcpUrl), {
51
+ fetch: signed.fetch, reconnectionOptions: { maxRetries: 0, initialReconnectionDelay: 1000, maxReconnectionDelay: 1000, reconnectionDelayGrowFactor: 1 },
52
+ });
53
+ const client = new Client({ name: "frely-cli-controller", version: VERSION });
54
+ try {
55
+ await client.connect(transport);
56
+ }
57
+ catch (error) {
58
+ await client.close().catch(() => undefined);
59
+ if (error instanceof ControllerError)
60
+ throw error;
61
+ throw new ControllerError(`Could not reach FrelyMCP: ${error instanceof Error ? error.message : String(error)}`, EXIT.offline);
62
+ }
63
+ return {
64
+ instructions: () => client.getInstructions(),
65
+ async tools() {
66
+ const all = [];
67
+ let cursor;
68
+ do {
69
+ const page = await client.listTools(cursor === undefined ? {} : { cursor });
70
+ all.push(...page.tools);
71
+ cursor = page.nextCursor;
72
+ } while (cursor && all.length < 1000);
73
+ return all;
74
+ },
75
+ call: (name, args) => client.callTool({ name, arguments: args }, undefined, { timeout: 180_000 }),
76
+ close: () => client.close(),
77
+ };
78
+ }
79
+ /** The tool's text parts joined, as a person or model would read them. */
80
+ export function resultText(result) {
81
+ const parts = Array.isArray(result.content) ? result.content : [];
82
+ return parts.filter((part) => part.type === "text" && typeof part.text === "string").map((part) => part.text).join("\n");
83
+ }
84
+ /** The result as one object: its structured content, else its text when that is a JSON object. */
85
+ export function resultData(result) {
86
+ if (result.structuredContent && typeof result.structuredContent === "object" && !Array.isArray(result.structuredContent))
87
+ return result.structuredContent;
88
+ try {
89
+ const parsed = JSON.parse(resultText(result));
90
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : null;
91
+ }
92
+ catch {
93
+ return null;
94
+ }
95
+ }
96
+ /**
97
+ * What a tool result means for the exit code. A pending approval (3) comes with where to approve and the command that waits for it; an
98
+ * offline device (5), a quota (6) and any other tool error (2) keep the server's own words in the output.
99
+ */
100
+ export function outcomeOf(result) {
101
+ const data = resultData(result);
102
+ if (data?.status === "pending" && (typeof data.approveUrl === "string" || typeof data.shortCode === "string")) {
103
+ const code = typeof data.shortCode === "string" ? data.shortCode : null;
104
+ const lines = [`Approval needed${typeof data.device === "string" ? ` on ${data.device}` : ""}.`];
105
+ if (typeof data.approveUrl === "string")
106
+ lines.push(`Approve here: ${data.approveUrl}`);
107
+ if (code)
108
+ lines.push(`After the owner approves, or to wait for it in the background: frely permission wait ${code}`);
109
+ return { exitCode: EXIT.approval, note: lines.join("\n") };
110
+ }
111
+ if (result.isError !== true)
112
+ return { exitCode: EXIT.ok };
113
+ const error = (data?.error && typeof data.error === "object" ? data.error : null);
114
+ if (error && (error.code === "permission_required" || error.code === "confirmation_required") && typeof error.url === "string") {
115
+ return { exitCode: EXIT.approval, note: `Approval needed. Approve here: ${error.url}` };
116
+ }
117
+ const text = resultText(result);
118
+ if (/frely mcp start|device is offline|Offline:/u.test(text))
119
+ return { exitCode: EXIT.offline };
120
+ if (error && typeof error.code === "string" && /limit|quota|insufficient/u.test(error.code))
121
+ return { exitCode: EXIT.limit };
122
+ return { exitCode: EXIT.tool };
123
+ }
124
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/controller/client.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,2CAA2C,CAAC;AACnE,OAAO,EAAE,6BAA6B,EAAE,MAAM,oDAAoD,CAAC;AACnG,OAAO,EAAE,OAAO,EAAE,MAAM,eAAe,CAAC;AACxC,OAAO,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAC5D,OAAO,EAAE,eAAe,EAAE,eAAe,EAAE,IAAI,EAAE,WAAW,EAAE,cAAc,EAA0B,MAAM,cAAc,CAAC;AAI3H;;;;GAIG;AACH,MAAM,UAAU,SAAS,CAAC,OAA0B,EAAE,UAAwG,EAAE;IAC9J,IAAI,OAAO,GAAG,OAAO,CAAC;IACtB,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,cAAc,CAAC;IAClD,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC;IACpC,MAAM,IAAI,GAAG,CAAC,KAA6B,EAAE,IAA6B,EAAE,EAAE;QAC5E,MAAM,GAAG,GAAG,KAAK,YAAY,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACjE,MAAM,MAAM,GAAG,IAAI,EAAE,MAAM,IAAI,CAAC,KAAK,YAAY,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;QACjF,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;QAC3C,OAAO,CAAC,GAAG,CAAC,eAAe,EAAE,QAAQ,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;QAC5D,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,cAAc,CAAC,OAAO,CAAC,aAAa,CAAC,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,WAAW,EAAE,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC;QAC/H,OAAO,KAAK,CAAC,GAAG,EAAE,EAAE,GAAG,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,CAAC;IAC7D,CAAC,CAAC;IACF,OAAO;QACL,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO;QACtB,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;YAC3B,IAAI,OAAO,CAAC,SAAS,GAAG,MAAM,IAAI,GAAG,EAAE;gBAAE,OAAO,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;YAC1E,IAAI,QAAQ,GAAG,MAAM,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;YACvC,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;gBAC5B,MAAM,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;gBACrD,OAAO,GAAG,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;gBACjC,QAAQ,GAAG,MAAM,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;gBACnC,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG;oBAAE,MAAM,IAAI,eAAe,CAAC,0HAA0H,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;YAChM,CAAC;YACD,OAAO,QAAQ,CAAC;QAClB,CAAC;KACF,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,MAAM,cAAc,GAAG,0KAA0K,CAAC;AAUzM,yHAAyH;AACzH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,UAAoE,EAAE,WAAW,EAAE,KAAK,EAAE;IAC7H,IAAI,OAAO,GAAG,MAAM,WAAW,EAAE,CAAC;IAClC,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,IAAI,CAAC,OAAO,CAAC,WAAW;YAAE,MAAM,IAAI,eAAe,CAAC,cAAc,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;QAC/E,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,IAAI,eAAe,CAAC,EAAE,CAAC;IACvD,CAAC;IACD,MAAM,MAAM,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC;IAClC,MAAM,SAAS,GAAG,IAAI,6BAA6B,CAAC,IAAI,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE;QAC3E,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,mBAAmB,EAAE,EAAE,UAAU,EAAE,CAAC,EAAE,wBAAwB,EAAE,IAAI,EAAE,oBAAoB,EAAE,IAAI,EAAE,2BAA2B,EAAE,CAAC,EAAE;KACxJ,CAAC,CAAC;IACH,MAAM,MAAM,GAAG,IAAI,MAAM,CAAC,EAAE,IAAI,EAAE,sBAAsB,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;IAC9E,IAAI,CAAC;QAAC,MAAM,MAAM,CAAC,OAAO,CAAC,SAAiC,CAAC,CAAC;IAAC,CAAC;IAChE,OAAO,KAAK,EAAE,CAAC;QACb,MAAM,MAAM,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QAC5C,IAAI,KAAK,YAAY,eAAe;YAAE,MAAM,KAAK,CAAC;QAClD,MAAM,IAAI,eAAe,CAAC,6BAA6B,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;IACjI,CAAC;IACD,OAAO;QACL,YAAY,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,eAAe,EAAE;QAC5C,KAAK,CAAC,KAAK;YACT,MAAM,GAAG,GAA6C,EAAE,CAAC;YACzD,IAAI,MAA0B,CAAC;YAC/B,GAAG,CAAC;gBACF,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,SAAS,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC;gBAC5E,GAAG,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC;gBACxB,MAAM,GAAG,IAAI,CAAC,UAAU,CAAC;YAC3B,CAAC,QAAQ,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,IAAI,EAAE;YACtC,OAAO,GAAG,CAAC;QACb,CAAC;QACD,IAAI,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,EAAE,SAAS,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,CAAwB;QACxH,KAAK,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK,EAAE;KAC5B,CAAC;AACJ,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,UAAU,CAAC,MAAkB;IAC3C,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,OAAoD,CAAC,CAAC,CAAC,EAAE,CAAC;IAC/G,OAAO,KAAK,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,KAAK,MAAM,IAAI,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAc,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACrI,CAAC;AAED,kGAAkG;AAClG,MAAM,UAAU,UAAU,CAAC,MAAkB;IAC3C,IAAI,MAAM,CAAC,iBAAiB,IAAI,OAAO,MAAM,CAAC,iBAAiB,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,iBAAiB,CAAC;QAAE,OAAO,MAAM,CAAC,iBAA4C,CAAC;IACrL,IAAI,CAAC;QACH,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC;QACvD,OAAO,MAAM,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAiC,CAAC,CAAC,CAAC,IAAI,CAAC;IACnH,CAAC;IAAC,MAAM,CAAC;QAAC,OAAO,IAAI,CAAC;IAAC,CAAC;AAC1B,CAAC;AAID;;;GAGG;AACH,MAAM,UAAU,SAAS,CAAC,MAAkB;IAC1C,MAAM,IAAI,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC;IAChC,IAAI,IAAI,EAAE,MAAM,KAAK,SAAS,IAAI,CAAC,OAAO,IAAI,CAAC,UAAU,KAAK,QAAQ,IAAI,OAAO,IAAI,CAAC,SAAS,KAAK,QAAQ,CAAC,EAAE,CAAC;QAC9G,MAAM,IAAI,GAAG,OAAO,IAAI,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC;QACxE,MAAM,KAAK,GAAG,CAAC,kBAAkB,OAAO,IAAI,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;QACjG,IAAI,OAAO,IAAI,CAAC,UAAU,KAAK,QAAQ;YAAE,KAAK,CAAC,IAAI,CAAC,iBAAiB,IAAI,CAAC,UAAU,EAAE,CAAC,CAAC;QACxF,IAAI,IAAI;YAAE,KAAK,CAAC,IAAI,CAAC,wFAAwF,IAAI,EAAE,CAAC,CAAC;QACrH,OAAO,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;IAC7D,CAAC;IACD,IAAI,MAAM,CAAC,OAAO,KAAK,IAAI;QAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC;IAC1D,MAAM,KAAK,GAAG,CAAC,IAAI,EAAE,KAAK,IAAI,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAA6C,CAAC;IAC9H,IAAI,KAAK,IAAI,CAAC,KAAK,CAAC,IAAI,KAAK,qBAAqB,IAAI,KAAK,CAAC,IAAI,KAAK,uBAAuB,CAAC,IAAI,OAAO,KAAK,CAAC,GAAG,KAAK,QAAQ,EAAE,CAAC;QAC/H,OAAO,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,EAAE,IAAI,EAAE,kCAAkC,KAAK,CAAC,GAAG,EAAE,EAAE,CAAC;IAC1F,CAAC;IACD,MAAM,IAAI,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC;IAChC,IAAI,6CAA6C,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,CAAC,OAAO,EAAE,CAAC;IAChG,IAAI,KAAK,IAAI,OAAO,KAAK,CAAC,IAAI,KAAK,QAAQ,IAAI,2BAA2B,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC;QAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC;IAC7H,OAAO,EAAE,QAAQ,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC;AACjC,CAAC"}
@@ -0,0 +1,16 @@
1
+ import { openController, type Controller } from "./client.js";
2
+ export declare const CONTROLLER_COMMANDS: ReadonlySet<string>;
3
+ export interface Output {
4
+ out(text: string): void;
5
+ err(text: string): void;
6
+ }
7
+ export declare const CONTROLLER_USAGE = "Control your Frely devices from this computer.\n frely devices [--add] List devices, whether they are online, and their workspaces\n frely run <device> [--cwd <dir>] [--timeout <s>] [--background] -- <command>\n frely ps <device> Background commands on the device\n frely logs <device> <id> [--follow] Output of a background command\n frely kill <device> <id> Stop a background command\n frely read <device>:<path> [--lines a:b] Print a file\n frely ls [<device>:<path>] List a folder (no argument: the workspace roots)\n frely grep <device>:<path> <text> [--regex] [--case] [--max <n>]\n frely edit <device>:<path> (--from <local file> | --patch <json file>) [--overwrite]\n frely permission request <device> --resource <name> --reason <text> --command <text> [--cwd <dir>] [--mode once|window|authorization]\n frely permission confirm <code> [A|B|C] Finish an approval by authenticator or emailed code\n frely permission wait <short code> [--then '<command>'] [--timeout <s>]\n frely tools [--json] What the server offers, with its own instructions\n frely call <tool> [--json '<object>' | --input <file>] Any tool by name\n frely controller login|logout|status This computer's authorization to control your devices\n\nEvery command takes --json (the full result) and --out <file> (write the output there).\nA device is its name or id from \"frely devices\". A path on this computer is not accepted where <device>:<path> is expected.\nExit codes: 0 done, 1 this CLI (usage, local file), 2 the tool or the remote command failed, 3 approval needed (the output has the link\nand the \"frely permission wait\" command), 4 not authorized (run frely controller login), 5 device offline, 6 limit reached.\n";
8
+ /** Tests replace the connection with a fake; returns the function that puts the real one back. */
9
+ export declare function useControllerOpener(replacement: typeof openController): () => void;
10
+ export declare function runControllerCommand(args: readonly string[], io: Output): Promise<number>;
11
+ /** Polls the request until the owner decides: approved is 0, denied 2, and a request that ran out (or was revoked) or the time limit is 3, because approval is still needed. */
12
+ export declare function waitForPermission(controller: Pick<Controller, "call">, code: string, timeoutMs: number, io: Output, options?: {
13
+ intervalMs?: number;
14
+ now?: () => number;
15
+ sleep?: (ms: number) => Promise<void>;
16
+ }): Promise<number>;
@@ -0,0 +1,411 @@
1
+ import { spawn } from "node:child_process";
2
+ import { readFile, stat, writeFile } from "node:fs/promises";
3
+ import { intOption, parseArgs, requireRemote } from "./args.js";
4
+ import { openController, outcomeOf, resultData, resultText } from "./client.js";
5
+ import { controllerLogin, controllerLogout, ControllerError, EXIT, readSession } from "./session.js";
6
+ export const CONTROLLER_COMMANDS = new Set(["devices", "run", "ps", "logs", "kill", "read", "ls", "grep", "edit", "tools", "call", "permission", "controller"]);
7
+ export const CONTROLLER_USAGE = `Control your Frely devices from this computer.
8
+ frely devices [--add] List devices, whether they are online, and their workspaces
9
+ frely run <device> [--cwd <dir>] [--timeout <s>] [--background] -- <command>
10
+ frely ps <device> Background commands on the device
11
+ frely logs <device> <id> [--follow] Output of a background command
12
+ frely kill <device> <id> Stop a background command
13
+ frely read <device>:<path> [--lines a:b] Print a file
14
+ frely ls [<device>:<path>] List a folder (no argument: the workspace roots)
15
+ frely grep <device>:<path> <text> [--regex] [--case] [--max <n>]
16
+ frely edit <device>:<path> (--from <local file> | --patch <json file>) [--overwrite]
17
+ frely permission request <device> --resource <name> --reason <text> --command <text> [--cwd <dir>] [--mode once|window|authorization]
18
+ frely permission confirm <code> [A|B|C] Finish an approval by authenticator or emailed code
19
+ frely permission wait <short code> [--then '<command>'] [--timeout <s>]
20
+ frely tools [--json] What the server offers, with its own instructions
21
+ frely call <tool> [--json '<object>' | --input <file>] Any tool by name
22
+ frely controller login|logout|status This computer's authorization to control your devices
23
+
24
+ Every command takes --json (the full result) and --out <file> (write the output there).
25
+ A device is its name or id from "frely devices". A path on this computer is not accepted where <device>:<path> is expected.
26
+ Exit codes: 0 done, 1 this CLI (usage, local file), 2 the tool or the remote command failed, 3 approval needed (the output has the link
27
+ and the "frely permission wait" command), 4 not authorized (run frely controller login), 5 device offline, 6 limit reached.
28
+ `;
29
+ const MAX_LOCAL_FILE = 1024 * 1024;
30
+ const isInteractive = () => process.stdin.isTTY === true && process.stderr.isTTY === true;
31
+ const sleep = (ms) => new Promise((done) => setTimeout(done, ms));
32
+ async function emit(shown, outcome, parsed, io) {
33
+ const body = parsed.flags.has("--json") ? JSON.stringify(shown.result, null, 2) : shown.text;
34
+ const file = parsed.values.get("--out");
35
+ if (file !== undefined) {
36
+ await writeFile(file, body.endsWith("\n") ? body : body + "\n");
37
+ io.out(`Wrote ${Buffer.byteLength(body)} bytes to ${file}\n`);
38
+ }
39
+ else
40
+ io.out(body === "" || body.endsWith("\n") ? body : body + "\n");
41
+ if (outcome.note)
42
+ io.err(outcome.note + "\n");
43
+ if (outcome.exitCode !== EXIT.ok)
44
+ return outcome.exitCode;
45
+ if (shown.remoteExit !== undefined && shown.remoteExit !== 0) {
46
+ io.err(`The command ended with exit code ${shown.remoteExit}.\n`);
47
+ return EXIT.tool;
48
+ }
49
+ return EXIT.ok;
50
+ }
51
+ const asText = (value) => typeof value === "string" ? value : "";
52
+ /** The output of a `process` or `read` result as the command itself would have printed it. */
53
+ function shownOf(result) {
54
+ const data = resultData(result);
55
+ if (data && (typeof data.stdout === "string" || typeof data.stderr === "string")) {
56
+ const text = asText(data.stdout) + (asText(data.stderr) ? (asText(data.stdout) && !asText(data.stdout).endsWith("\n") ? "\n" : "") + asText(data.stderr) : "");
57
+ const exitCode = typeof data.exitCode === "number" ? data.exitCode : undefined;
58
+ return { text, result, ...(exitCode !== undefined ? { remoteExit: exitCode } : {}) };
59
+ }
60
+ if (data && typeof data.content === "string" && data.encoding === undefined)
61
+ return { text: data.content, result };
62
+ return { text: resultText(result) || (data ? JSON.stringify(data, null, 2) : ""), result };
63
+ }
64
+ let openIt = openController;
65
+ /** Tests replace the connection with a fake; returns the function that puts the real one back. */
66
+ export function useControllerOpener(replacement) {
67
+ const previous = openIt;
68
+ openIt = replacement;
69
+ return () => { openIt = previous; };
70
+ }
71
+ async function withController(io, work) {
72
+ const controller = await openIt({ interactive: isInteractive() });
73
+ try {
74
+ return await work(controller);
75
+ }
76
+ finally {
77
+ await controller.close().catch(() => undefined);
78
+ }
79
+ }
80
+ const COMMON_FLAGS = ["--json"];
81
+ const COMMON_VALUES = ["--out"];
82
+ async function readLocal(path, what) {
83
+ const info = await stat(path).catch(() => { throw new ControllerError(`${what} ${path} was not found.`, EXIT.cli); });
84
+ if (!info.isFile())
85
+ throw new ControllerError(`${what} ${path} is not a file.`, EXIT.cli);
86
+ if (info.size > MAX_LOCAL_FILE)
87
+ throw new ControllerError(`${what} ${path} is larger than 1 MiB.`, EXIT.cli);
88
+ return readFile(path);
89
+ }
90
+ export async function runControllerCommand(args, io) {
91
+ const [command, ...rest] = args;
92
+ try {
93
+ if (command === undefined || rest.includes("--help") || rest.includes("-h")) {
94
+ io.out(CONTROLLER_USAGE);
95
+ return EXIT.ok;
96
+ }
97
+ switch (command) {
98
+ case "controller": return await controllerCommand(rest, io);
99
+ case "permission": return await permissionCommand(rest, io);
100
+ case "devices": return await devices(rest, io);
101
+ case "run": return await run(rest, io);
102
+ case "ps": return await ps(rest, io);
103
+ case "logs": return await logs(rest, io);
104
+ case "kill": return await kill(rest, io);
105
+ case "read": return await readCommand(rest, io);
106
+ case "ls": return await ls(rest, io);
107
+ case "grep": return await grep(rest, io);
108
+ case "edit": return await edit(rest, io);
109
+ case "tools": return await toolsCommand(rest, io);
110
+ case "call": return await callCommand(rest, io);
111
+ default: throw new ControllerError(`Unknown command ${command}.\n${CONTROLLER_USAGE}`, EXIT.cli);
112
+ }
113
+ }
114
+ catch (error) {
115
+ if (error instanceof ControllerError) {
116
+ io.err(error.message + "\n");
117
+ return error.exitCode;
118
+ }
119
+ throw error;
120
+ }
121
+ }
122
+ async function controllerCommand(args, io) {
123
+ const action = args[0];
124
+ if (action === "login") {
125
+ const session = await controllerLogin({ notify: (text) => io.err(text) });
126
+ io.out(`This computer can now control your Frely devices (FrelyMCP ${session.mcpUrl}).\n`);
127
+ return EXIT.ok;
128
+ }
129
+ if (action === "logout") {
130
+ io.out(await controllerLogout() ? "This computer no longer controls your Frely devices.\n" : "This computer was not authorized as a controller.\n");
131
+ return EXIT.ok;
132
+ }
133
+ if (action === "status") {
134
+ const session = await readSession();
135
+ io.out(session ? `Authorized to control your devices: yes\nFrelyMCP: ${session.mcpUrl}\n` : "Authorized to control your devices: no. Run frely controller login.\n");
136
+ return EXIT.ok;
137
+ }
138
+ throw new ControllerError("Usage: frely controller login|logout|status", EXIT.cli);
139
+ }
140
+ async function devices(args, io) {
141
+ const parsed = parseArgs(args, { flags: [...COMMON_FLAGS, "--add"], values: COMMON_VALUES });
142
+ return withController(io, async (controller) => {
143
+ const result = await controller.call("list_devices", parsed.flags.has("--add") ? { addDevice: true } : {});
144
+ const outcome = outcomeOf(result);
145
+ const data = resultData(result);
146
+ const list = Array.isArray(data?.devices) ? data.devices : null;
147
+ let text = resultText(result);
148
+ if (list) {
149
+ const rows = list.map((device) => `${asText(device.name) || asText(device.id)}\t${device.online === true ? "online" : "offline"}\t${asText(device.mcp)}\t${Array.isArray(device.workspaces) ? device.workspaces.join(", ") : "-"}`);
150
+ text = ["NAME\tSTATE\tMCP\tWORKSPACES", ...rows].join("\n") + (typeof data?.setup === "string" ? `\n\n${data.setup}` : "");
151
+ }
152
+ return emit({ text, result }, outcome, parsed, io);
153
+ });
154
+ }
155
+ async function toolCall(io, parsed, name, input) {
156
+ return withController(io, async (controller) => {
157
+ const result = await controller.call(name, input);
158
+ return emit(shownOf(result), outcomeOf(result), parsed, io);
159
+ });
160
+ }
161
+ async function run(args, io) {
162
+ const parsed = parseArgs(args, { flags: [...COMMON_FLAGS, "--background"], values: [...COMMON_VALUES, "--cwd", "--timeout"] });
163
+ const [device] = parsed.positionals;
164
+ if (!device || parsed.positionals.length !== 1 || parsed.rest.length === 0)
165
+ throw new ControllerError("Usage: frely run <device> [--cwd <dir>] [--timeout <s>] [--background] -- <command>", EXIT.cli);
166
+ const timeout = intOption(parsed, "--timeout", 1, 120);
167
+ const background = parsed.flags.has("--background");
168
+ return toolCall(io, parsed, "process", {
169
+ device, action: background ? "start" : "run", command: parsed.rest.join(" "), ...(parsed.values.has("--cwd") ? { cwd: parsed.values.get("--cwd") } : {}),
170
+ ...(timeout !== undefined && !background ? { timeoutMs: timeout * 1000 } : {}),
171
+ });
172
+ }
173
+ async function ps(args, io) {
174
+ const parsed = parseArgs(args, { flags: COMMON_FLAGS, values: COMMON_VALUES });
175
+ if (parsed.positionals.length !== 1)
176
+ throw new ControllerError("Usage: frely ps <device>", EXIT.cli);
177
+ return toolCall(io, parsed, "process", { device: parsed.positionals[0], action: "list" });
178
+ }
179
+ async function kill(args, io) {
180
+ const parsed = parseArgs(args, { flags: COMMON_FLAGS, values: COMMON_VALUES });
181
+ if (parsed.positionals.length !== 2)
182
+ throw new ControllerError("Usage: frely kill <device> <id>", EXIT.cli);
183
+ return toolCall(io, parsed, "process", { device: parsed.positionals[0], action: "stop", processId: parsed.positionals[1] });
184
+ }
185
+ async function logs(args, io) {
186
+ const parsed = parseArgs(args, { flags: [...COMMON_FLAGS, "--follow"], values: COMMON_VALUES });
187
+ if (parsed.positionals.length !== 2)
188
+ throw new ControllerError("Usage: frely logs <device> <id> [--follow]", EXIT.cli);
189
+ const [device, processId] = parsed.positionals;
190
+ const follow = parsed.flags.has("--follow");
191
+ return withController(io, async (controller) => {
192
+ let stdoutCursor = 0, stderrCursor = 0;
193
+ for (;;) {
194
+ const result = await controller.call("process", { device, action: "read", processId, stdoutCursor, stderrCursor });
195
+ const outcome = outcomeOf(result);
196
+ const data = resultData(result);
197
+ if (!follow || outcome.exitCode !== EXIT.ok || !data)
198
+ return emit(shownOf(result), outcome, parsed, io);
199
+ const out = asText(data.stdout), err = asText(data.stderr);
200
+ if (out)
201
+ io.out(out);
202
+ if (err)
203
+ io.err(err);
204
+ stdoutCursor = typeof data.stdoutCursor === "number" ? data.stdoutCursor : stdoutCursor;
205
+ stderrCursor = typeof data.stderrCursor === "number" ? data.stderrCursor : stderrCursor;
206
+ if (data.running === false)
207
+ return typeof data.exitCode === "number" && data.exitCode !== 0 ? EXIT.tool : EXIT.ok;
208
+ await sleep(1000);
209
+ }
210
+ });
211
+ }
212
+ async function readCommand(args, io) {
213
+ const parsed = parseArgs(args, { flags: COMMON_FLAGS, values: [...COMMON_VALUES, "--lines"] });
214
+ if (parsed.positionals.length !== 1)
215
+ throw new ControllerError("Usage: frely read <device>:<path> [--lines a:b]", EXIT.cli);
216
+ const { device, path } = requireRemote(parsed.positionals[0]);
217
+ let range = {};
218
+ const lines = parsed.values.get("--lines");
219
+ if (lines !== undefined) {
220
+ const match = /^(\d+):(\d*)$/u.exec(lines);
221
+ if (!match || Number(match[1]) < 1)
222
+ throw new ControllerError("--lines must look like 10:40 (or 10: for the rest of the file).", EXIT.cli);
223
+ range = { startLine: Number(match[1]), ...(match[2] ? { endLine: Number(match[2]) } : {}) };
224
+ }
225
+ return toolCall(io, parsed, "read", { device, action: "read", path, ...range });
226
+ }
227
+ async function ls(args, io) {
228
+ const parsed = parseArgs(args, { flags: COMMON_FLAGS, values: COMMON_VALUES });
229
+ if (parsed.positionals.length > 1)
230
+ throw new ControllerError("Usage: frely ls [<device>:<path>]", EXIT.cli);
231
+ const [target] = parsed.positionals;
232
+ if (target !== undefined && !target.includes(":"))
233
+ return toolCall(io, parsed, "read", { device: target, action: "roots" });
234
+ const { device, path } = requireRemote(target);
235
+ return toolCall(io, parsed, "read", { device, action: "list", path });
236
+ }
237
+ async function grep(args, io) {
238
+ const parsed = parseArgs(args, { flags: [...COMMON_FLAGS, "--regex", "--case"], values: [...COMMON_VALUES, "--max", "--context"] });
239
+ if (parsed.positionals.length !== 2)
240
+ throw new ControllerError("Usage: frely grep <device>:<path> <text> [--regex] [--case] [--max <n>] [--context <n>]", EXIT.cli);
241
+ const { device, path } = requireRemote(parsed.positionals[0]);
242
+ const max = intOption(parsed, "--max", 1, 1000), context = intOption(parsed, "--context", 0, 10);
243
+ return toolCall(io, parsed, "read", {
244
+ device, action: "search", path, query: parsed.positionals[1], regex: parsed.flags.has("--regex"), caseSensitive: parsed.flags.has("--case"),
245
+ ...(max !== undefined ? { maxResults: max } : {}), ...(context !== undefined ? { contextLines: context } : {}),
246
+ });
247
+ }
248
+ async function edit(args, io) {
249
+ const parsed = parseArgs(args, { flags: [...COMMON_FLAGS, "--overwrite"], values: [...COMMON_VALUES, "--from", "--patch"] });
250
+ if (parsed.positionals.length !== 1)
251
+ throw new ControllerError("Usage: frely edit <device>:<path> (--from <local file> | --patch <json file>) [--overwrite]", EXIT.cli);
252
+ const { device, path } = requireRemote(parsed.positionals[0]);
253
+ const from = parsed.values.get("--from"), patch = parsed.values.get("--patch");
254
+ if ((from === undefined) === (patch === undefined))
255
+ throw new ControllerError("Give exactly one of --from <local file> or --patch <json file>.", EXIT.cli);
256
+ let input;
257
+ if (from !== undefined) {
258
+ const data = await readLocal(from, "The file");
259
+ let text = null;
260
+ try {
261
+ text = new TextDecoder("utf-8", { fatal: true }).decode(data);
262
+ }
263
+ catch { /* Sent as binary below. */ }
264
+ input = { device, path, ...(text !== null ? { content: text } : { contentBase64: data.toString("base64") }), overwrite: parsed.flags.has("--overwrite") };
265
+ }
266
+ else {
267
+ let edits;
268
+ try {
269
+ edits = JSON.parse((await readLocal(patch, "The patch")).toString("utf8"));
270
+ }
271
+ catch (error) {
272
+ if (error instanceof ControllerError)
273
+ throw error;
274
+ throw new ControllerError(`The patch ${patch} is not valid JSON.`, EXIT.cli);
275
+ }
276
+ if (!Array.isArray(edits) || edits.length === 0)
277
+ throw new ControllerError("The patch must be a JSON array of { startLine, endLine, replacement }.", EXIT.cli);
278
+ input = { device, path, edits };
279
+ }
280
+ return toolCall(io, parsed, "edit_file", input);
281
+ }
282
+ async function toolsCommand(args, io) {
283
+ const parsed = parseArgs(args, { flags: COMMON_FLAGS, values: COMMON_VALUES });
284
+ return withController(io, async (controller) => {
285
+ const tools = await controller.tools();
286
+ const instructions = controller.instructions() ?? "";
287
+ const result = { content: [], structuredContent: { instructions, tools } };
288
+ const text = [instructions, ...tools.map((tool) => `${tool.name}${tool.annotations?.readOnlyHint ? " [read]" : ""} ${(tool.description ?? "").replace(/\s+/gu, " ").slice(0, 160)}`)].filter(Boolean).join("\n\n");
289
+ return emit({ text, result: { ...result } }, { exitCode: EXIT.ok }, parsed, io);
290
+ });
291
+ }
292
+ /** `--json` takes a JSON object here (as in frely api call); a bare --json still asks for the full result. */
293
+ function callArgs(args) {
294
+ const rest = [];
295
+ let json;
296
+ for (let i = 0; i < args.length; i++) {
297
+ const next = args[i + 1];
298
+ if (args[i] === "--json" && next !== undefined && /^\s*[{[]/u.test(next)) {
299
+ json = next;
300
+ i++;
301
+ }
302
+ else
303
+ rest.push(args[i]);
304
+ }
305
+ const parsed = parseArgs(rest, { flags: COMMON_FLAGS, values: [...COMMON_VALUES, "--input"] });
306
+ if (json !== undefined && parsed.values.has("--input"))
307
+ throw new ControllerError("Use --json '<object>' or --input <file>, not both.", EXIT.cli);
308
+ return { parsed, input: json !== undefined ? parseObject(json) : null };
309
+ }
310
+ function parseObject(text) {
311
+ let value;
312
+ try {
313
+ value = JSON.parse(text);
314
+ }
315
+ catch {
316
+ throw new ControllerError("The input is not valid JSON.", EXIT.cli);
317
+ }
318
+ if (!value || typeof value !== "object" || Array.isArray(value))
319
+ throw new ControllerError("The input must be a JSON object.", EXIT.cli);
320
+ return value;
321
+ }
322
+ async function callCommand(args, io) {
323
+ const { parsed, input } = callArgs(args);
324
+ const [name] = parsed.positionals;
325
+ if (!name || parsed.positionals.length !== 1 || !/^[a-zA-Z0-9_.:-]{1,128}$/u.test(name))
326
+ throw new ControllerError("Usage: frely call <tool> [--json '<object>' | --input <file>]", EXIT.cli);
327
+ const file = parsed.values.get("--input");
328
+ const arguments_ = input ?? (file !== undefined ? parseObject((await readLocal(file, "The input")).toString("utf8")) : {});
329
+ return toolCall(io, parsed, name, arguments_);
330
+ }
331
+ async function permissionCommand(args, io) {
332
+ const [action, ...rest] = args;
333
+ if (action === "request") {
334
+ const parsed = parseArgs(rest, { flags: [...COMMON_FLAGS, "--no-wait"], values: [...COMMON_VALUES, "--resource", "--reason", "--command", "--cwd", "--mode"] });
335
+ const [device] = parsed.positionals;
336
+ const resource = parsed.values.get("--resource"), reason = parsed.values.get("--reason"), command = parsed.values.get("--command");
337
+ if (!device || parsed.positionals.length !== 1 || !resource || !reason || !command)
338
+ throw new ControllerError("Usage: frely permission request <device> --resource <name> --reason <text> --command <text> [--cwd <dir>] [--mode once|window|authorization]", EXIT.cli);
339
+ const mode = parsed.values.get("--mode");
340
+ if (mode !== undefined && !["once", "window", "authorization"].includes(mode))
341
+ throw new ControllerError("--mode must be once, window or authorization.", EXIT.cli);
342
+ return withController(io, async (controller) => {
343
+ const result = await controller.call("request_permission", { device, resource, reason, command, ...(parsed.values.has("--cwd") ? { cwd: parsed.values.get("--cwd") } : {}), ...(mode ? { mode } : {}) });
344
+ const outcome = outcomeOf(result);
345
+ // A person at a terminal waits here for the approval; a script or an AI gets the link and the wait command, and the exit code 3.
346
+ const code = resultData(result)?.shortCode;
347
+ if (outcome.exitCode !== EXIT.approval || typeof code !== "string" || !isInteractive() || parsed.flags.has("--no-wait") || parsed.flags.has("--json"))
348
+ return emit(shownOf(result), outcome, parsed, io);
349
+ io.out(resultText(result) + "\n");
350
+ io.err("Waiting for the owner to decide…\n");
351
+ return waitForPermission(controller, code, 10 * 60_000, io);
352
+ });
353
+ }
354
+ if (action === "confirm") {
355
+ const parsed = parseArgs(rest, { flags: COMMON_FLAGS, values: COMMON_VALUES });
356
+ const [code, choice] = parsed.positionals;
357
+ if (!code || parsed.positionals.length > 2 || (choice !== undefined && !/^[ABC]$/u.test(choice)))
358
+ throw new ControllerError("Usage: frely permission confirm <code> [A|B|C]", EXIT.cli);
359
+ return toolCall(io, parsed, "confirm_permission", { code, ...(choice ? { choice } : {}) });
360
+ }
361
+ if (action === "wait") {
362
+ const parsed = parseArgs(rest, { values: ["--then", "--timeout"] });
363
+ const [code] = parsed.positionals;
364
+ if (!code || parsed.positionals.length !== 1 || !/^[A-Za-z]{6}$/u.test(code))
365
+ throw new ControllerError("Usage: frely permission wait <six-letter short code> [--then '<command>'] [--timeout <s>]", EXIT.cli);
366
+ const timeout = intOption(parsed, "--timeout", 1, 3600) ?? 600;
367
+ const status = await withController(io, (controller) => waitForPermission(controller, code.toUpperCase(), timeout * 1000, io));
368
+ const then = parsed.values.get("--then");
369
+ if (status !== EXIT.ok || then === undefined)
370
+ return status;
371
+ // The approval of one command is bound to its exact text, so the command is run as given.
372
+ return new Promise((done, fail) => {
373
+ const child = spawn(then, { shell: true, stdio: "inherit" });
374
+ child.once("error", fail);
375
+ child.once("exit", (exit) => done(exit ?? EXIT.tool));
376
+ });
377
+ }
378
+ throw new ControllerError("Usage: frely permission request|confirm|wait. See frely permission --help.", EXIT.cli);
379
+ }
380
+ /** Polls the request until the owner decides: approved is 0, denied 2, and a request that ran out (or was revoked) or the time limit is 3, because approval is still needed. */
381
+ export async function waitForPermission(controller, code, timeoutMs, io, options = {}) {
382
+ const { intervalMs = 3000, now = Date.now, sleep: wait = sleep } = options;
383
+ const deadline = now() + timeoutMs;
384
+ for (;;) {
385
+ const result = await controller.call("permission_status", { code });
386
+ const data = resultData(result);
387
+ if (result.isError === true || !data) {
388
+ io.err((resultText(result) || "The approval could not be checked.") + "\n");
389
+ return EXIT.tool;
390
+ }
391
+ switch (data.status) {
392
+ case "approved":
393
+ io.out(`Approved${typeof data.mode === "string" ? ` (${data.mode})` : ""}${typeof data.expiresAt === "string" ? ` until ${data.expiresAt}` : ""}. Run the command again unchanged.\n`);
394
+ return EXIT.ok;
395
+ case "denied":
396
+ io.err("The owner denied this request.\n");
397
+ return EXIT.tool;
398
+ case "expired":
399
+ case "revoked":
400
+ io.err(`The request is ${String(data.status)}. Ask for it again.\n`);
401
+ return EXIT.approval;
402
+ default: break;
403
+ }
404
+ if (now() + intervalMs >= deadline) {
405
+ io.err("Still waiting for the owner. Run frely permission wait again to keep waiting.\n");
406
+ return EXIT.approval;
407
+ }
408
+ await wait(intervalMs);
409
+ }
410
+ }
411
+ //# sourceMappingURL=commands.js.map