doer-agent 0.2.6 → 0.2.7

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 (2) hide show
  1. package/dist/agent.js +106 -237
  2. package/package.json +1 -1
package/dist/agent.js CHANGED
@@ -9,16 +9,13 @@ const AGENT_MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
9
9
  const AGENT_PROJECT_DIR = path.join(AGENT_MODULE_DIR, "..");
10
10
  const AGENT_PACKAGE_JSON_PATH = path.join(AGENT_PROJECT_DIR, "package.json");
11
11
  let activeTaskLogContext = null;
12
- const activeTaskCancelRequests = new Map();
13
12
  let workspaceRootOverride = null;
14
13
  const fsRpcCodec = StringCodec();
15
- const shellRpcCodec = StringCodec();
16
14
  const runRpcCodec = StringCodec();
17
15
  const sessionRpcCodec = StringCodec();
18
16
  const codexAuthRpcCodec = StringCodec();
19
17
  const settingsRpcCodec = StringCodec();
20
18
  const gitRpcCodec = StringCodec();
21
- const activeRuns = new Map();
22
19
  const retainedRuns = new Map();
23
20
  const activeSessionWatchers = new Map();
24
21
  const sessionLineIndexCache = new Map();
@@ -466,10 +463,13 @@ async function persistRunTask(task) {
466
463
  runId: task.id,
467
464
  agentId: task.agentId,
468
465
  userId: task.userId,
466
+ processPid: task.processPid,
469
467
  sessionId: task.sessionId,
470
468
  sessionFilePath: task.sessionFilePath,
471
469
  status: task.status,
472
470
  cancelRequested: task.cancelRequested,
471
+ resultExitCode: task.resultExitCode,
472
+ resultSignal: task.resultSignal,
473
473
  createdAt: task.createdAt,
474
474
  updatedAt: task.updatedAt,
475
475
  startedAt: task.startedAt,
@@ -1020,10 +1020,71 @@ async function updateRunSessionMetadata(task, metadata) {
1020
1020
  function persistRetainedRun(task) {
1021
1021
  retainedRuns.set(task.id, cloneRunTask(task));
1022
1022
  }
1023
- function getStoredRun(runId) {
1024
- const active = activeRuns.get(runId);
1025
- if (active) {
1026
- return active.task;
1023
+ function normalizePersistedRunTask(value) {
1024
+ if (!value || typeof value !== "object") {
1025
+ return null;
1026
+ }
1027
+ const record = value;
1028
+ const id = typeof record.runId === "string" && record.runId.trim()
1029
+ ? record.runId.trim()
1030
+ : typeof record.id === "string" && record.id.trim()
1031
+ ? record.id.trim()
1032
+ : "";
1033
+ const userId = typeof record.userId === "string" ? record.userId : "";
1034
+ const agentId = typeof record.agentId === "string" ? record.agentId : "";
1035
+ const status = record.status;
1036
+ if (!id || !userId || !agentId || !["queued", "running", "completed", "failed", "canceled"].includes(String(status))) {
1037
+ return null;
1038
+ }
1039
+ return {
1040
+ id,
1041
+ userId,
1042
+ agentId,
1043
+ processPid: typeof record.processPid === "number" ? record.processPid : null,
1044
+ sessionId: typeof record.sessionId === "string" && record.sessionId.trim() ? record.sessionId.trim() : null,
1045
+ sessionFilePath: typeof record.sessionFilePath === "string" && record.sessionFilePath.trim() ? record.sessionFilePath.trim() : null,
1046
+ status: status,
1047
+ cancelRequested: Boolean(record.cancelRequested),
1048
+ resultExitCode: typeof record.resultExitCode === "number" ? record.resultExitCode : null,
1049
+ resultSignal: typeof record.resultSignal === "string" && record.resultSignal.trim() ? record.resultSignal.trim() : null,
1050
+ error: typeof record.error === "string" && record.error.trim() ? record.error : null,
1051
+ createdAt: typeof record.createdAt === "string" ? record.createdAt : "",
1052
+ updatedAt: typeof record.updatedAt === "string" ? record.updatedAt : "",
1053
+ startedAt: typeof record.startedAt === "string" && record.startedAt.trim() ? record.startedAt : null,
1054
+ finishedAt: typeof record.finishedAt === "string" && record.finishedAt.trim() ? record.finishedAt : null,
1055
+ };
1056
+ }
1057
+ async function listPersistedRunTasks() {
1058
+ const dir = await resolveRunsDir();
1059
+ const names = await readdir(dir).catch(() => []);
1060
+ const tasks = await Promise.all(names
1061
+ .filter((name) => name.endsWith(".json"))
1062
+ .map(async (name) => {
1063
+ const raw = await readFile(path.join(dir, name), "utf8").catch(() => null);
1064
+ if (!raw) {
1065
+ return null;
1066
+ }
1067
+ try {
1068
+ return normalizePersistedRunTask(JSON.parse(raw));
1069
+ }
1070
+ catch {
1071
+ return null;
1072
+ }
1073
+ }));
1074
+ return tasks.filter((task) => task !== null);
1075
+ }
1076
+ async function getStoredRun(runId) {
1077
+ const persisted = await readFile(path.join(await resolveRunsDir(), `${runId}.json`), "utf8").catch(() => null);
1078
+ if (persisted) {
1079
+ try {
1080
+ const parsed = normalizePersistedRunTask(JSON.parse(persisted));
1081
+ if (parsed) {
1082
+ return parsed;
1083
+ }
1084
+ }
1085
+ catch {
1086
+ // Ignore malformed persisted state and fall back to retained memory.
1087
+ }
1027
1088
  }
1028
1089
  return retainedRuns.get(runId) ?? null;
1029
1090
  }
@@ -1045,6 +1106,7 @@ async function startManagedRun(args) {
1045
1106
  id: args.runId,
1046
1107
  userId: args.userId,
1047
1108
  agentId: args.agentId,
1109
+ processPid: typeof child.pid === "number" ? child.pid : null,
1048
1110
  sessionId: typeof args.sessionId === "string" && args.sessionId.trim() ? args.sessionId.trim() : null,
1049
1111
  sessionFilePath: null,
1050
1112
  status: "running",
@@ -1057,17 +1119,6 @@ async function startManagedRun(args) {
1057
1119
  startedAt: now,
1058
1120
  finishedAt: null,
1059
1121
  };
1060
- const cancellation = createManagedCancellation(child);
1061
- const requestCancel = () => {
1062
- if (task.status === "completed" || task.status === "failed" || task.status === "canceled") {
1063
- return;
1064
- }
1065
- task.cancelRequested = true;
1066
- task.updatedAt = formatLocalTimestamp();
1067
- void persistRunTask(task).catch(() => undefined);
1068
- writeRunStatus(task.id, "cancel requested");
1069
- cancellation.requestCancel();
1070
- };
1071
1122
  let stdoutBuffer = "";
1072
1123
  const recordChunk = (stream, chunk) => {
1073
1124
  writeRunStream(task.id, stream, chunk);
@@ -1096,33 +1147,33 @@ async function startManagedRun(args) {
1096
1147
  task.error = message;
1097
1148
  task.finishedAt = formatLocalTimestamp();
1098
1149
  persistRetainedRun(task);
1099
- activeRuns.delete(task.id);
1100
1150
  void removeRunTask(task.id).catch(() => undefined);
1101
1151
  void releaseRunStartSlot({ runId: task.id, sessionId: task.sessionId }).catch(() => undefined);
1102
1152
  void prepared.codexAuthCleanup().catch(() => undefined);
1103
1153
  writeRunStatus(task.id, `failed error=${message}`);
1104
1154
  });
1105
- child.once("close", (code, signal) => {
1106
- cancellation.clear();
1155
+ child.once("close", async (code, signal) => {
1107
1156
  if (stdoutBuffer.trim() && (!task.sessionId || !task.sessionFilePath)) {
1108
1157
  const metadata = extractCodexSessionMetadata(stdoutBuffer.trim());
1109
1158
  if (metadata.sessionId || metadata.sessionFilePath) {
1110
1159
  void updateRunSessionMetadata(task, metadata);
1111
1160
  }
1112
1161
  }
1162
+ const latest = await getStoredRun(task.id).catch(() => null);
1163
+ if (latest?.cancelRequested) {
1164
+ task.cancelRequested = true;
1165
+ }
1113
1166
  task.resultExitCode = typeof code === "number" ? code : null;
1114
1167
  task.resultSignal = signal;
1115
1168
  task.finishedAt = formatLocalTimestamp();
1116
1169
  task.status = task.cancelRequested ? "canceled" : (task.resultExitCode ?? 1) === 0 ? "completed" : "failed";
1117
1170
  task.error = task.status === "failed" ? `Command exited with code ${task.resultExitCode ?? "null"}` : null;
1118
1171
  persistRetainedRun(task);
1119
- activeRuns.delete(task.id);
1120
1172
  void removeRunTask(task.id).catch(() => undefined);
1121
1173
  void releaseRunStartSlot({ runId: task.id, sessionId: task.sessionId }).catch(() => undefined);
1122
1174
  void prepared.codexAuthCleanup().catch(() => undefined);
1123
1175
  writeRunStatus(task.id, `completed status=${task.status} exitCode=${task.resultExitCode ?? "null"} signal=${task.resultSignal ?? "null"}`);
1124
1176
  });
1125
- activeRuns.set(task.id, { task, child, requestCancel });
1126
1177
  persistRetainedRun(task);
1127
1178
  void persistRunTask(task).catch(() => undefined);
1128
1179
  writeRunStatus(task.id, `started requestId=${args.requestId} cwd=${prepared.taskWorkspace}`);
@@ -1935,22 +1986,37 @@ async function handleRunRpcMessage(args) {
1935
1986
  return;
1936
1987
  }
1937
1988
  if (request.action === "list") {
1938
- const tasks = [...activeRuns.values()].map((entry) => cloneRunTask(entry.task));
1939
- const retained = [...retainedRuns.values()].filter((task) => !activeRuns.has(task.id)).map((task) => cloneRunTask(task));
1940
- const merged = [...tasks, ...retained]
1989
+ const persisted = await listPersistedRunTasks();
1990
+ const mergedById = new Map();
1991
+ for (const task of persisted) {
1992
+ mergedById.set(task.id, cloneRunTask(task));
1993
+ }
1994
+ for (const task of retainedRuns.values()) {
1995
+ if (!mergedById.has(task.id)) {
1996
+ mergedById.set(task.id, cloneRunTask(task));
1997
+ }
1998
+ }
1999
+ const merged = [...mergedById.values()]
1941
2000
  .sort((a, b) => Date.parse(b.updatedAt) - Date.parse(a.updatedAt))
1942
2001
  .slice(0, request.limit);
1943
2002
  publishRunRpcResponse({ nc: args.jetstream.nc, responseSubject, payload: { requestId, ok: true, tasks: merged } });
1944
2003
  return;
1945
2004
  }
1946
- const stored = request.runId ? getStoredRun(request.runId) : null;
2005
+ const stored = request.runId ? await getStoredRun(request.runId) : null;
1947
2006
  if (!stored || stored.agentId !== args.agentId || stored.userId !== args.userId) {
1948
2007
  throw new Error("Run not found");
1949
2008
  }
1950
2009
  if (request.action === "cancel") {
1951
- const active = activeRuns.get(stored.id);
1952
- active?.requestCancel();
1953
- const task = cloneRunTask(active?.task ?? stored);
2010
+ const target = stored;
2011
+ if (target.processPid === null) {
2012
+ throw new Error("Run pid not found");
2013
+ }
2014
+ target.cancelRequested = true;
2015
+ target.updatedAt = formatLocalTimestamp();
2016
+ await persistRunTask(target);
2017
+ writeRunStatus(target.id, `cancel requested pid=${target.processPid}`);
2018
+ sendSignalToPid(target.processPid, "SIGINT");
2019
+ const task = cloneRunTask(target);
1954
2020
  publishRunRpcResponse({ nc: args.jetstream.nc, responseSubject, payload: { requestId, ok: true, task } });
1955
2021
  return;
1956
2022
  }
@@ -2026,21 +2092,17 @@ function sendSignalToTaskProcess(child, signal) {
2026
2092
  // noop
2027
2093
  }
2028
2094
  }
2029
- function requestTaskCancellation(taskId, reason) {
2030
- const requestCancel = activeTaskCancelRequests.get(taskId);
2031
- if (!requestCancel) {
2032
- return false;
2033
- }
2034
- try {
2035
- requestCancel();
2036
- writeAgentInfo(`task cancel requested taskId=${taskId} via=${reason}`);
2037
- return true;
2038
- }
2039
- catch (error) {
2040
- const message = error instanceof Error ? error.message : String(error);
2041
- writeAgentError(`task cancel request failed taskId=${taskId} via=${reason}: ${message}`);
2042
- return false;
2095
+ function sendSignalToPid(pid, signal) {
2096
+ if (process.platform !== "win32") {
2097
+ try {
2098
+ process.kill(-pid, signal);
2099
+ return;
2100
+ }
2101
+ catch {
2102
+ // Fall back to direct pid signaling.
2103
+ }
2043
2104
  }
2105
+ process.kill(pid, signal);
2044
2106
  }
2045
2107
  function resolveLogTimeZone() {
2046
2108
  const configured = process.env.DOER_AGENT_LOG_TIMEZONE?.trim() || process.env.TZ?.trim();
@@ -2172,9 +2234,6 @@ function resolveTaskWorkspace(rawCwd) {
2172
2234
  function buildAgentFsRpcSubject(userId, agentId) {
2173
2235
  return `doer.agent.fs.rpc.${sanitizeUserId(userId)}.${agentId.trim()}`;
2174
2236
  }
2175
- function buildAgentShellRpcSubject(userId, agentId) {
2176
- return `doer.agent.shell.rpc.${sanitizeUserId(userId)}.${agentId.trim()}`;
2177
- }
2178
2237
  function normalizeFsRpcPath(rawPath) {
2179
2238
  const root = workspaceRootOverride ?? (process.env.WORKSPACE?.trim() || process.cwd());
2180
2239
  const raw = typeof rawPath === "string" && rawPath.trim() ? rawPath.trim() : ".";
@@ -3188,46 +3247,6 @@ function subscribeToFsRpc(args) {
3188
3247
  });
3189
3248
  writeAgentInfo(`fs rpc subscribed subject=${subject}`);
3190
3249
  }
3191
- function normalizeShellRpcRequest(args) {
3192
- const requestId = typeof args.request.requestId === "string" ? args.request.requestId.trim() : "";
3193
- if (!requestId) {
3194
- throw new Error("missing requestId");
3195
- }
3196
- const requestAgentId = typeof args.request.agentId === "string" ? args.request.agentId.trim() : "";
3197
- if (!requestAgentId) {
3198
- throw new Error("missing agentId");
3199
- }
3200
- if (requestAgentId !== args.agentId) {
3201
- throw new Error("agent id mismatch");
3202
- }
3203
- const kind = args.request.kind === "apply_patch" ? "apply_patch" : "shell";
3204
- const command = typeof args.request.command === "string" ? args.request.command.trim() : "";
3205
- const patch = typeof args.request.patch === "string" ? args.request.patch : "";
3206
- if (kind === "shell" && !command) {
3207
- throw new Error("missing command");
3208
- }
3209
- if (kind === "apply_patch" && !patch.trim()) {
3210
- throw new Error("missing patch");
3211
- }
3212
- const responseSubject = typeof args.request.responseSubject === "string" ? args.request.responseSubject.trim() : "";
3213
- if (!responseSubject) {
3214
- throw new Error("missing responseSubject");
3215
- }
3216
- const cwd = typeof args.request.cwd === "string" && args.request.cwd.trim() ? args.request.cwd.trim() : null;
3217
- const timeoutRaw = Number(args.request.timeoutMs);
3218
- const timeoutMs = Number.isFinite(timeoutRaw) ? Math.max(1000, Math.min(Math.floor(timeoutRaw), 300000)) : 30000;
3219
- return {
3220
- kind,
3221
- requestId,
3222
- command: kind === "shell" ? command : null,
3223
- patch: kind === "apply_patch" ? patch : null,
3224
- cwd,
3225
- timeoutMs,
3226
- responseSubject,
3227
- runtimeEnvPatch: normalizeEnvPatch(args.request.runtimeEnvPatch),
3228
- codexAuthBundle: normalizeShellRpcCodexAuthBundle(args.request.codexAuth),
3229
- };
3230
- }
3231
3250
  function normalizeShellRpcCodexAuthBundle(value) {
3232
3251
  if (!value || typeof value !== "object" || Array.isArray(value)) {
3233
3252
  return null;
@@ -3246,118 +3265,6 @@ function normalizeShellRpcCodexAuthBundle(value) {
3246
3265
  apiKey: typeof row.apiKey === "string" || row.apiKey === null ? row.apiKey : undefined,
3247
3266
  };
3248
3267
  }
3249
- function publishShellRpcResponse(args) {
3250
- args.nc.publish(args.responseSubject, shellRpcCodec.encode(JSON.stringify(args.payload)));
3251
- }
3252
- async function handleShellRpcMessage(args) {
3253
- let requestId = "unknown";
3254
- let responseSubject = "";
3255
- let stdout = "";
3256
- let stderr = "";
3257
- try {
3258
- const payload = JSON.parse(shellRpcCodec.decode(args.msg.data));
3259
- const request = normalizeShellRpcRequest({ request: payload, agentId: args.agentId });
3260
- requestId = request.requestId;
3261
- responseSubject = request.responseSubject;
3262
- const startedAtMs = Date.now();
3263
- const prepared = await prepareCommandExecution({
3264
- cwd: request.cwd,
3265
- userId: args.userId,
3266
- taskId: request.requestId,
3267
- codexAuthBundle: request.codexAuthBundle,
3268
- });
3269
- const child = spawnPreparedCommand({
3270
- kind: request.kind,
3271
- command: request.command,
3272
- patch: request.patch,
3273
- shellPath: prepared.shellPath,
3274
- taskWorkspace: prepared.taskWorkspace,
3275
- env: prepared.env,
3276
- agentToken: args.agentToken,
3277
- });
3278
- writeRpcStatus(requestId, `started kind=${request.kind} cwd=${prepared.taskWorkspace} shell=${request.kind === "shell" ? prepared.shellPath : "apply_patch"}`);
3279
- child.stdout.on("data", (chunk) => {
3280
- stdout += chunk;
3281
- writeRpcStream(requestId, "stdout", chunk);
3282
- });
3283
- child.stderr.on("data", (chunk) => {
3284
- stderr += chunk;
3285
- writeRpcStream(requestId, "stderr", chunk);
3286
- });
3287
- let timedOut = false;
3288
- const timeout = setTimeout(() => {
3289
- timedOut = true;
3290
- sendSignalToTaskProcess(child, "SIGTERM");
3291
- setTimeout(() => {
3292
- sendSignalToTaskProcess(child, "SIGKILL");
3293
- }, 1000).unref?.();
3294
- }, request.timeoutMs);
3295
- timeout.unref?.();
3296
- const result = await new Promise((resolve, reject) => {
3297
- child.once("error", reject);
3298
- child.once("close", (code, signal) => {
3299
- resolve({ exitCode: typeof code === "number" ? code : null, signal });
3300
- });
3301
- }).finally(() => {
3302
- clearTimeout(timeout);
3303
- });
3304
- await prepared.codexAuthCleanup().catch(() => undefined);
3305
- publishShellRpcResponse({
3306
- nc: args.jetstream.nc,
3307
- responseSubject,
3308
- payload: {
3309
- requestId,
3310
- ok: !timedOut,
3311
- exitCode: result.exitCode,
3312
- signal: result.signal,
3313
- stdout,
3314
- stderr,
3315
- ...(timedOut ? { error: `Command timed out after ${request.timeoutMs}ms` } : {}),
3316
- },
3317
- });
3318
- writeRpcStatus(requestId, `${timedOut ? "timed_out" : "completed"} exitCode=${result.exitCode ?? "null"} signal=${result.signal ?? "null"} durationMs=${Date.now() - startedAtMs}`);
3319
- }
3320
- catch (error) {
3321
- const message = error instanceof Error ? error.message : String(error);
3322
- if (responseSubject) {
3323
- publishShellRpcResponse({
3324
- nc: args.jetstream.nc,
3325
- responseSubject,
3326
- payload: {
3327
- requestId,
3328
- ok: false,
3329
- exitCode: null,
3330
- signal: null,
3331
- stdout,
3332
- stderr,
3333
- error: message,
3334
- },
3335
- });
3336
- }
3337
- writeRpcStatus(requestId, `failed error=${message}`);
3338
- writeAgentError(`shell rpc failed requestId=${requestId} error=${message}`);
3339
- }
3340
- }
3341
- function subscribeToShellRpc(args) {
3342
- const subject = buildAgentShellRpcSubject(args.userId, args.agentId);
3343
- args.jetstream.nc.subscribe(subject, {
3344
- callback: (error, msg) => {
3345
- if (error) {
3346
- const message = error instanceof Error ? error.message : String(error);
3347
- writeAgentError(`shell rpc subscription error: ${message}`);
3348
- return;
3349
- }
3350
- void handleShellRpcMessage({
3351
- msg,
3352
- jetstream: args.jetstream,
3353
- userId: args.userId,
3354
- agentId: args.agentId,
3355
- agentToken: args.agentToken,
3356
- });
3357
- },
3358
- });
3359
- writeAgentInfo(`shell rpc subscribed subject=${subject}`);
3360
- }
3361
3268
  async function postJson(url, body) {
3362
3269
  const res = await fetch(url, {
3363
3270
  method: "POST",
@@ -3605,36 +3512,6 @@ function spawnPreparedCommand(args) {
3605
3512
  child.stderr.setEncoding("utf8");
3606
3513
  return child;
3607
3514
  }
3608
- function createManagedCancellation(child) {
3609
- let cancelStage1Timer = null;
3610
- let cancelStage2Timer = null;
3611
- let cancelSignalSent = false;
3612
- return {
3613
- requestCancel: () => {
3614
- if (cancelSignalSent) {
3615
- return;
3616
- }
3617
- cancelSignalSent = true;
3618
- sendSignalToTaskProcess(child, "SIGINT");
3619
- cancelStage1Timer = setTimeout(() => {
3620
- sendSignalToTaskProcess(child, "SIGTERM");
3621
- }, 1200);
3622
- cancelStage1Timer.unref?.();
3623
- cancelStage2Timer = setTimeout(() => {
3624
- sendSignalToTaskProcess(child, "SIGKILL");
3625
- }, 3500);
3626
- cancelStage2Timer.unref?.();
3627
- },
3628
- clear: () => {
3629
- if (cancelStage1Timer) {
3630
- clearTimeout(cancelStage1Timer);
3631
- }
3632
- if (cancelStage2Timer) {
3633
- clearTimeout(cancelStage2Timer);
3634
- }
3635
- },
3636
- };
3637
- }
3638
3515
  async function runTask(args) {
3639
3516
  activeTaskLogContext = {
3640
3517
  jetstream: args.jetstream,
@@ -3728,7 +3605,6 @@ async function runTask(args) {
3728
3605
  }, 3500);
3729
3606
  cancelStage2Timer.unref?.();
3730
3607
  };
3731
- activeTaskCancelRequests.set(args.taskId, requestCancel);
3732
3608
  child.stdout.on("data", (chunk) => {
3733
3609
  writeTaskStream(args.taskId, "stdout", chunk);
3734
3610
  const seq = reserveNextEventSeq(args.taskId);
@@ -3823,7 +3699,6 @@ async function runTask(args) {
3823
3699
  writeAgentInfo(`task=${args.taskId} status=${status} exitCode=${typeof result.code === "number" ? result.code : "null"} signal=${result.signal ?? "null"}`);
3824
3700
  }
3825
3701
  finally {
3826
- activeTaskCancelRequests.delete(args.taskId);
3827
3702
  activeTaskLogContext = null;
3828
3703
  await codexAuth?.cleanup().catch(() => undefined);
3829
3704
  }
@@ -3927,12 +3802,6 @@ async function main() {
3927
3802
  agentId: initialAgentId,
3928
3803
  agentToken,
3929
3804
  });
3930
- subscribeToShellRpc({
3931
- jetstream,
3932
- userId,
3933
- agentId: initialAgentId,
3934
- agentToken,
3935
- });
3936
3805
  subscribeToSessionRpc({
3937
3806
  jetstream,
3938
3807
  userId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doer-agent",
3
- "version": "0.2.6",
3
+ "version": "0.2.7",
4
4
  "description": "Reverse-polling agent runtime for doer",
5
5
  "type": "module",
6
6
  "main": "dist/agent.js",