@github/copilot-sdk 1.0.15-preview.2 → 1.0.15-preview.4
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/README.md +53 -25
- package/dist/cjs/cliVersion.js +1 -1
- package/dist/cjs/client.js +21 -6
- package/dist/cjs/extension.js +1 -12
- package/dist/cjs/generated/rpc.js +307 -177
- package/dist/cjs/index.js +0 -7
- package/dist/cjs/installationConfirmation.js +98 -0
- package/dist/cjs/session.js +15 -524
- package/dist/cliVersion.d.ts +1 -1
- package/dist/cliVersion.js +1 -1
- package/dist/client.d.ts +1 -0
- package/dist/client.js +23 -6
- package/dist/extension.d.ts +10 -24
- package/dist/extension.js +1 -13
- package/dist/generated/rpc.d.ts +4455 -2788
- package/dist/generated/rpc.js +307 -177
- package/dist/generated/session-events.d.ts +211 -120
- package/dist/index.d.ts +2 -4
- package/dist/index.js +0 -4
- package/dist/installationConfirmation.d.ts +19 -0
- package/dist/installationConfirmation.js +77 -0
- package/dist/session.d.ts +3 -25
- package/dist/session.js +15 -529
- package/dist/types.d.ts +31 -66
- package/dist/workflow.d.ts +5 -2
- package/docs/extensions.md +0 -1
- package/docs/workflows.md +2 -4
- package/package.json +10 -10
- package/dist/cjs/factory.js +0 -134
- package/dist/factory.d.ts +0 -327
- package/dist/factory.js +0 -106
- package/docs/factories.md +0 -296
- package/docs/factory-patterns.md +0 -194
package/README.md
CHANGED
|
@@ -18,11 +18,6 @@ The checked-in `copilotCliVersion` in `package.json` and compiled metadata in
|
|
|
18
18
|
`src/cliVersion.ts` use a development placeholder. The public SDK snapshot
|
|
19
19
|
replaces both with the CLI version published for that snapshot.
|
|
20
20
|
|
|
21
|
-
Do not change these pins for runtime-repository development. If they contain
|
|
22
|
-
`0.0.0-dev`, use the same-checkout runtime; release snapshot export owns replacing
|
|
23
|
-
development placeholders with a published CLI version. See
|
|
24
|
-
[checkout preparation](../CONTRIBUTING.md#testing-an-unreleased-runtime-api).
|
|
25
|
-
|
|
26
21
|
`npm run pack:release` builds the main package and all platform packages. Set
|
|
27
22
|
`COPILOT_CLI_DOWNLOAD_BASE_URL` to use a release mirror while packaging.
|
|
28
23
|
Release workflows instead set `COPILOT_SDK_RUNTIME_PACKAGE_DIR` to a directory
|
|
@@ -44,11 +39,7 @@ npm install @github/copilot-sdk
|
|
|
44
39
|
|
|
45
40
|
## Run the Sample
|
|
46
41
|
|
|
47
|
-
Try the interactive chat sample from the
|
|
48
|
-
In the runtime repository, first run `pnpm run build:cli` from the runtime root
|
|
49
|
-
to prepare the same-checkout executable, then return to `src/sdk`.
|
|
50
|
-
Building the Node SDK alone does not build the runtime. For dependency
|
|
51
|
-
prerequisites, see [development setup](#development).
|
|
42
|
+
Try the interactive chat sample (from the repo root):
|
|
52
43
|
|
|
53
44
|
```bash
|
|
54
45
|
cd nodejs
|
|
@@ -133,6 +124,7 @@ new CopilotClient(options?: CopilotClientOptions)
|
|
|
133
124
|
- `workingDirectory?: string` - Working directory for the runtime process (default: current process cwd).
|
|
134
125
|
- `baseDirectory?: string` - Base directory for Copilot data (session state, config, etc.). Sets `COPILOT_HOME` on the spawned runtime. When not set, the runtime defaults to `~/.copilot`. Ignored when connecting via `RuntimeConnection.forUri`.
|
|
135
126
|
- `extensionLaunchProvider?: ExtensionLaunchProvider` - Experimental connection-level resolver for extension launch profiles. The client installs the reverse-RPC handler and registers the provider during startup before sessions can be created.
|
|
127
|
+
- `installationConfirmationHandler?: InstallationConfirmationHandler` - Experimental connection-global human review for `installations.confirm`. Receives the typed request and independent request/connection cancellation signals, and returns an explicit decision. Does not enable installation capabilities.
|
|
136
128
|
- `logLevel?: "none" | "error" | "warning" | "info" | "debug" | "all"` - Log level. When omitted, the runtime uses its own default (currently `"info"`).
|
|
137
129
|
- `env?: Record<string, string | undefined>` - Environment variables for the runtime process. When omitted, inherits `process.env`.
|
|
138
130
|
- `gitHubToken?: string` - GitHub token for authentication. When provided, takes priority over other auth methods.
|
|
@@ -144,6 +136,49 @@ new CopilotClient(options?: CopilotClientOptions)
|
|
|
144
136
|
- `sessionIdleTimeoutSeconds?: number` - Server-wide idle timeout for sessions in seconds. Ignored when connecting via `RuntimeConnection.forUri`.
|
|
145
137
|
- `enableRemoteSessions?: boolean` - Enable Mission Control remote session support. Ignored when connecting via `RuntimeConnection.forUri`.
|
|
146
138
|
|
|
139
|
+
#### Installation confirmation (experimental)
|
|
140
|
+
|
|
141
|
+
All six SDKs (Node.js, Python, Go, .NET, Java and Rust) provide this receiver with the
|
|
142
|
+
same semantics: each review gets one cancellation token, concurrent reviews are
|
|
143
|
+
independent, and a decision returned after cancellation is never sent. Without a
|
|
144
|
+
configured handler, an `installations.confirm` request is refused, which the
|
|
145
|
+
runtime treats as no consent.
|
|
146
|
+
|
|
147
|
+
The installation confirmation handler receives the generated
|
|
148
|
+
`InstallationConfirmationRequest` and a `CancellationToken`. Match `operationId`
|
|
149
|
+
and `policySessionId` against the exact original action on this connection
|
|
150
|
+
before presenting the complete review. Refuse unknown operations or incomplete
|
|
151
|
+
reviews; missing legacy session metadata is not permission to use the current
|
|
152
|
+
session. Return `"confirm"`, `"decline"` or `"cancel"` only after an explicit
|
|
153
|
+
human decision. The SDK echoes the original challenge and fingerprint.
|
|
154
|
+
|
|
155
|
+
Concurrent reviews remain independent. The token is cancelled when the runtime
|
|
156
|
+
retires the request, including runtime-enforced expiry, or when the original
|
|
157
|
+
connection closes. Observe it to close pending UI. Late handler results cannot
|
|
158
|
+
approve a retired request. This incoming signal does not cancel outbound
|
|
159
|
+
installation or OAuth RPCs, and dropping those promises is not cancellation.
|
|
160
|
+
|
|
161
|
+
Use `client.rpc.mcp.prepareInstall` before `applyInstall`: preparation returns an
|
|
162
|
+
inert runtime-issued `operationId` and original expiry. Register that ID with its
|
|
163
|
+
captured session on this exact client before applying. Removal uses
|
|
164
|
+
`planUninstall` then `applyUninstall`; the returned `operationId` identifies the
|
|
165
|
+
operation, while `planHandle` is the one-use removal input. Never interchange them.
|
|
166
|
+
Use `client.rpc.mcp.installations.list` and `recover` for owned inventory.
|
|
167
|
+
Inspect or cancel uncertain work through `status` and `cancel` on the original
|
|
168
|
+
connection and operation ID, without selecting a replacement session or replaying apply.
|
|
169
|
+
|
|
170
|
+
Owned OAuth similarly uses `session.rpc.mcp.oauth.prepareLogin` to obtain
|
|
171
|
+
`loginId` before browser, network or cached-reconnect work. Retain that ID with
|
|
172
|
+
the original session and `expectedInstallationId` for `login` and `cancelLogin`.
|
|
173
|
+
Prepare freezes reauthentication and display options. Cancelling an incoming
|
|
174
|
+
confirmation or abandoning a login promise is not a substitute for `cancelLogin`.
|
|
175
|
+
Manual MCP OAuth retains its direct `login` path.
|
|
176
|
+
|
|
177
|
+
A matching runtime contract and available owned-lifecycle support are required.
|
|
178
|
+
Capability negotiation does not promise availability; preserve typed refusals
|
|
179
|
+
instead of falling back to raw configuration writes. Generated presence and
|
|
180
|
+
transport tests do not establish a working installer, live OAuth or restart safety.
|
|
181
|
+
|
|
147
182
|
#### Methods
|
|
148
183
|
|
|
149
184
|
##### `start(): Promise<void>`
|
|
@@ -172,6 +207,7 @@ Create a new conversation session.
|
|
|
172
207
|
- `systemMessage?: SystemMessageConfig` - System message customization (see below)
|
|
173
208
|
- `infiniteSessions?: InfiniteSessionConfig` - Configure automatic context compaction (see below)
|
|
174
209
|
- `workingDirectory?: string` - Working directory for the session (default: runtime process cwd).
|
|
210
|
+
- `refreshCustomInstructions?: boolean` - Invalidates the process-wide custom-instruction discovery cache before creating this session, so instruction-file edits made in the same runtime are read again. Defaults to `false` (cache reuse). Other sessions in this runtime may observe updated instructions on later turns or discovery. This does not watch files or enable disabled instruction loading. Available on `SessionConfig`, not `ResumeSessionConfig`.
|
|
175
211
|
- `enableSessionStore?: boolean` - Enables the cross-session store for search and retrieval across sessions. When unset in `"copilot-cli"` mode, the runtime default applies (enabled). In `"empty"` mode, defaults to disabled.
|
|
176
212
|
- `gitHubTokenProvider?: GitHubTokenProvider` - Acquires rotating, session-scoped GitHub tokens. Token results require a positive `expiresIn` value in seconds remaining when the callback completes; production tokens typically last eight hours. Cannot be combined with `gitHubToken`.
|
|
177
213
|
- `provider?: ProviderConfig` - Custom API provider configuration (BYOK - Bring Your Own Key). See [Custom Providers](#custom-providers) section.
|
|
@@ -309,6 +345,7 @@ Source is independent of delivery mode. Leaving it unset preserves the existing
|
|
|
309
345
|
##### `sendAndWait(options: MessageOptions, timeout?: number): Promise<AssistantMessageEvent | undefined>`
|
|
310
346
|
|
|
311
347
|
Send a message and wait until the session becomes idle.
|
|
348
|
+
Sub-agent events are still delivered to listeners, but do not complete the wait or supply its reply.
|
|
312
349
|
|
|
313
350
|
**Options:**
|
|
314
351
|
|
|
@@ -1288,28 +1325,19 @@ try {
|
|
|
1288
1325
|
|
|
1289
1326
|
## Development
|
|
1290
1327
|
|
|
1291
|
-
|
|
1292
|
-
including the harness and corrections-script dependencies. From the SDK root
|
|
1293
|
-
(`src/sdk` in the runtime repository, or the standalone repository root):
|
|
1328
|
+
From the repository root:
|
|
1294
1329
|
|
|
1295
1330
|
```bash
|
|
1296
|
-
|
|
1297
|
-
npm
|
|
1298
|
-
npm run check:nodejs
|
|
1331
|
+
cd test/harness
|
|
1332
|
+
npm ci
|
|
1299
1333
|
```
|
|
1300
1334
|
|
|
1301
|
-
In the runtime layout, these build/test commands refresh the projection and
|
|
1302
|
-
prepare the checked-out runtime for tests. For focused unit tests after
|
|
1303
|
-
installing Node dependencies:
|
|
1304
|
-
|
|
1305
1335
|
```bash
|
|
1306
|
-
|
|
1336
|
+
cd nodejs
|
|
1337
|
+
npm ci
|
|
1338
|
+
npm test
|
|
1307
1339
|
```
|
|
1308
1340
|
|
|
1309
|
-
For native Vitest selectors on E2Es, use the
|
|
1310
|
-
[prepared-runtime instructions](../CONTRIBUTING.md#testing-an-unreleased-runtime-api);
|
|
1311
|
-
the SDK facade does not forward selectors.
|
|
1312
|
-
|
|
1313
1341
|
## License
|
|
1314
1342
|
|
|
1315
1343
|
MIT
|
package/dist/cjs/cliVersion.js
CHANGED
|
@@ -22,7 +22,7 @@ __export(cliVersion_exports, {
|
|
|
22
22
|
COPILOT_CLI_VERSION: () => COPILOT_CLI_VERSION
|
|
23
23
|
});
|
|
24
24
|
module.exports = __toCommonJS(cliVersion_exports);
|
|
25
|
-
const COPILOT_CLI_VERSION = "1.0.89-
|
|
25
|
+
const COPILOT_CLI_VERSION = "1.0.89-7";
|
|
26
26
|
const COPILOT_CLI_USE_NPM_PACKAGE = false;
|
|
27
27
|
// Annotate the CommonJS export names for ESM import in node:
|
|
28
28
|
0 && (module.exports = {
|
package/dist/cjs/client.js
CHANGED
|
@@ -44,6 +44,7 @@ var import_runtimeArtifacts = require("./runtimeArtifacts.js");
|
|
|
44
44
|
var import_cliVersion = require("./cliVersion.js");
|
|
45
45
|
var import_sessionFsProvider = require("./sessionFsProvider.js");
|
|
46
46
|
var import_copilotRequestHandler = require("./copilotRequestHandler.js");
|
|
47
|
+
var import_installationConfirmation = require("./installationConfirmation.js");
|
|
47
48
|
var import_telemetry = require("./telemetry.js");
|
|
48
49
|
var import_schema = require("./schema.js");
|
|
49
50
|
var import_toolSet = require("./toolSet.js");
|
|
@@ -265,6 +266,7 @@ class CopilotClient {
|
|
|
265
266
|
sessionFsConfig = null;
|
|
266
267
|
requestHandler = null;
|
|
267
268
|
extensionLaunchProvider;
|
|
269
|
+
installationConfirmationHandler;
|
|
268
270
|
builtinPluginDirectories = [];
|
|
269
271
|
onGitHubTelemetry;
|
|
270
272
|
clientGlobalHandlers = {};
|
|
@@ -423,6 +425,7 @@ class CopilotClient {
|
|
|
423
425
|
this.sessionFsConfig = options.sessionFs ?? null;
|
|
424
426
|
this.requestHandler = options.requestHandler ?? null;
|
|
425
427
|
this.extensionLaunchProvider = options.extensionLaunchProvider;
|
|
428
|
+
this.installationConfirmationHandler = options.installationConfirmationHandler;
|
|
426
429
|
this.onGitHubTelemetry = options.onGitHubTelemetry;
|
|
427
430
|
this.setupClientGlobalHandlers();
|
|
428
431
|
const connEnv = conn.kind === "stdio" || conn.kind === "tcp" ? conn.env : void 0;
|
|
@@ -1237,6 +1240,7 @@ class CopilotClient {
|
|
|
1237
1240
|
includeSubAgentStreamingEvents: config.includeSubAgentStreamingEvents ?? true,
|
|
1238
1241
|
...this.onGitHubTelemetry != null ? { enableGitHubTelemetryForwarding: true } : {},
|
|
1239
1242
|
mcpServers: toWireMcpServers(config.mcpServers),
|
|
1243
|
+
...config.diagnostics !== void 0 ? { diagnostics: config.diagnostics } : {},
|
|
1240
1244
|
mcpOAuthTokenStorage: config.mcpOAuthTokenStorage,
|
|
1241
1245
|
authClientIdMetadataUrl: config.authClientIdMetadataUrl,
|
|
1242
1246
|
envValueMode: "direct",
|
|
@@ -1246,6 +1250,7 @@ class CopilotClient {
|
|
|
1246
1250
|
agent: config.agent,
|
|
1247
1251
|
configDir: config.configDirectory,
|
|
1248
1252
|
enableConfigDiscovery: config.enableConfigDiscovery,
|
|
1253
|
+
refreshCustomInstructions: config.refreshCustomInstructions,
|
|
1249
1254
|
skipEmbeddingRetrieval: config.skipEmbeddingRetrieval,
|
|
1250
1255
|
embeddingCacheStorage: config.embeddingCacheStorage,
|
|
1251
1256
|
organizationCustomInstructions: config.organizationCustomInstructions,
|
|
@@ -1337,14 +1342,12 @@ class CopilotClient {
|
|
|
1337
1342
|
async resumeSession(sessionId, config) {
|
|
1338
1343
|
return this.resumeSessionInternal(sessionId, config);
|
|
1339
1344
|
}
|
|
1345
|
+
/** @internal */
|
|
1340
1346
|
async resumeSessionForExtension(sessionId, config, contributions = {}, extensionOptions) {
|
|
1341
1347
|
return this.resumeSessionInternal(sessionId, config, contributions, extensionOptions);
|
|
1342
1348
|
}
|
|
1343
1349
|
async resumeSessionInternal(sessionId, config, contributions = {}, extensionOptions) {
|
|
1344
|
-
const {
|
|
1345
|
-
if (factories !== void 0 && workflows !== void 0) {
|
|
1346
|
-
throw new Error("Session configuration cannot include both factories and workflows");
|
|
1347
|
-
}
|
|
1350
|
+
const { workflows } = contributions;
|
|
1348
1351
|
if (config.gitHubToken !== void 0 && config.gitHubTokenProvider !== void 0) {
|
|
1349
1352
|
throw new Error("gitHubToken and gitHubTokenProvider are mutually exclusive");
|
|
1350
1353
|
}
|
|
@@ -1364,7 +1367,6 @@ class CopilotClient {
|
|
|
1364
1367
|
session.registerTools(config.tools);
|
|
1365
1368
|
session.registerCanvases(config.canvases);
|
|
1366
1369
|
session.registerCommands(config.commands);
|
|
1367
|
-
session.registerFactories(factories);
|
|
1368
1370
|
session.registerWorkflows(workflows);
|
|
1369
1371
|
const {
|
|
1370
1372
|
wireProvider: bearerWireProvider,
|
|
@@ -1446,7 +1448,6 @@ class CopilotClient {
|
|
|
1446
1448
|
})),
|
|
1447
1449
|
toolSearch: config.toolSearch,
|
|
1448
1450
|
canvases: config.canvases?.map((canvas) => canvas.declaration),
|
|
1449
|
-
factories: factories?.map((factory) => factory.meta),
|
|
1450
1451
|
workflows: workflows?.map((workflow) => workflow.meta),
|
|
1451
1452
|
requestCanvasRenderer: config.requestCanvasRenderer,
|
|
1452
1453
|
requestExtensions: config.requestExtensions,
|
|
@@ -1488,6 +1489,7 @@ class CopilotClient {
|
|
|
1488
1489
|
includeSubAgentStreamingEvents: config.includeSubAgentStreamingEvents ?? true,
|
|
1489
1490
|
...this.onGitHubTelemetry != null ? { enableGitHubTelemetryForwarding: true } : {},
|
|
1490
1491
|
mcpServers: toWireMcpServers(config.mcpServers),
|
|
1492
|
+
...config.diagnostics !== void 0 ? { diagnostics: config.diagnostics } : {},
|
|
1491
1493
|
mcpOAuthTokenStorage: config.mcpOAuthTokenStorage,
|
|
1492
1494
|
authClientIdMetadataUrl: config.authClientIdMetadataUrl,
|
|
1493
1495
|
envValueMode: "direct",
|
|
@@ -1908,6 +1910,9 @@ class CopilotClient {
|
|
|
1908
1910
|
if (this.options.mode === "empty") {
|
|
1909
1911
|
env.COPILOT_DISABLE_KEYTAR = "1";
|
|
1910
1912
|
}
|
|
1913
|
+
if (this.options.mode !== "empty") {
|
|
1914
|
+
env.COPILOT_RUNTIME_PROCESS_FILE_LOGGING = "1";
|
|
1915
|
+
}
|
|
1911
1916
|
if (this.options.telemetry) {
|
|
1912
1917
|
const t = this.options.telemetry;
|
|
1913
1918
|
env.COPILOT_OTEL_ENABLED = "true";
|
|
@@ -2282,6 +2287,16 @@ stderr: ${stderrOutput}` : ""}`
|
|
|
2282
2287
|
return session.clientSessionApis;
|
|
2283
2288
|
});
|
|
2284
2289
|
(0, import_rpc.registerClientGlobalApiHandlers)(this.connection, this.clientGlobalHandlers);
|
|
2290
|
+
if (this.installationConfirmationHandler) {
|
|
2291
|
+
const confirmation = (0, import_installationConfirmation.createInstallationConfirmationAdapter)(
|
|
2292
|
+
this.connection,
|
|
2293
|
+
this.installationConfirmationHandler
|
|
2294
|
+
);
|
|
2295
|
+
this.connection.onRequest(
|
|
2296
|
+
"installations.confirm",
|
|
2297
|
+
(params, token) => confirmation.confirm(params, token)
|
|
2298
|
+
);
|
|
2299
|
+
}
|
|
2285
2300
|
this.connection.onRequest(
|
|
2286
2301
|
"hooks.invoke",
|
|
2287
2302
|
async (params) => {
|
package/dist/cjs/extension.js
CHANGED
|
@@ -20,12 +20,9 @@ var extension_exports = {};
|
|
|
20
20
|
__export(extension_exports, {
|
|
21
21
|
Canvas: () => import_canvas.Canvas,
|
|
22
22
|
CanvasError: () => import_canvas.CanvasError,
|
|
23
|
-
FactoryResumeError: () => import_factory.FactoryResumeError,
|
|
24
23
|
WorkflowResumeError: () => import_workflow.WorkflowResumeError,
|
|
25
24
|
createCanvas: () => import_canvas.createCanvas,
|
|
26
|
-
defineFactory: () => import_factory.defineFactory,
|
|
27
25
|
defineWorkflow: () => import_workflow.defineWorkflow,
|
|
28
|
-
isFactoryRunTerminal: () => import_factory.isFactoryRunTerminal,
|
|
29
26
|
isWorkflowRunTerminal: () => import_workflow.isWorkflowRunTerminal,
|
|
30
27
|
joinSession: () => joinSession
|
|
31
28
|
});
|
|
@@ -33,7 +30,6 @@ module.exports = __toCommonJS(extension_exports);
|
|
|
33
30
|
var import_client = require("./client.js");
|
|
34
31
|
var import_types = require("./types.js");
|
|
35
32
|
var import_canvas = require("./canvas.js");
|
|
36
|
-
var import_factory = require("./factory.js");
|
|
37
33
|
var import_workflow = require("./workflow.js");
|
|
38
34
|
async function joinSession(config = {}) {
|
|
39
35
|
const sessionId = process.env.SESSION_ID;
|
|
@@ -45,15 +41,11 @@ async function joinSession(config = {}) {
|
|
|
45
41
|
const client = new import_client.CopilotClient({ _internalConnection: { kind: "parent-process" } });
|
|
46
42
|
const {
|
|
47
43
|
extensionSdkPath: _stripped,
|
|
48
|
-
factories,
|
|
49
44
|
workflows,
|
|
50
45
|
requestedEnvironmentVariables,
|
|
51
46
|
...rest
|
|
52
47
|
} = config;
|
|
53
48
|
void _stripped;
|
|
54
|
-
if (factories !== void 0 && workflows !== void 0) {
|
|
55
|
-
throw new Error("joinSession cannot register both factories and workflows");
|
|
56
|
-
}
|
|
57
49
|
return client.resumeSessionForExtension(
|
|
58
50
|
sessionId,
|
|
59
51
|
{
|
|
@@ -61,7 +53,7 @@ async function joinSession(config = {}) {
|
|
|
61
53
|
onPermissionRequest: config.onPermissionRequest ?? import_types.defaultJoinSessionPermissionHandler,
|
|
62
54
|
suppressResumeEvent: config.suppressResumeEvent ?? true
|
|
63
55
|
},
|
|
64
|
-
{
|
|
56
|
+
{ workflows },
|
|
65
57
|
requestedEnvironmentVariables?.length ? { requestedEnvironmentVariables } : void 0
|
|
66
58
|
);
|
|
67
59
|
}
|
|
@@ -69,12 +61,9 @@ async function joinSession(config = {}) {
|
|
|
69
61
|
0 && (module.exports = {
|
|
70
62
|
Canvas,
|
|
71
63
|
CanvasError,
|
|
72
|
-
FactoryResumeError,
|
|
73
64
|
WorkflowResumeError,
|
|
74
65
|
createCanvas,
|
|
75
|
-
defineFactory,
|
|
76
66
|
defineWorkflow,
|
|
77
|
-
isFactoryRunTerminal,
|
|
78
67
|
isWorkflowRunTerminal,
|
|
79
68
|
joinSession
|
|
80
69
|
});
|