@saadnvd1/agent-os 0.3.664 → 0.3.672

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 (95) hide show
  1. package/app/api/audits/[kind]/[id]/route.test.ts +2 -2
  2. package/app/api/mcp-auth/callback/route.test.ts +42 -0
  3. package/app/api/mcp-auth/callback/route.ts +56 -0
  4. package/app/api/projects/[id]/route.ts +26 -0
  5. package/app/api/projects/init/route.test.ts +2 -4
  6. package/app/api/sessions/[id]/mcp-auth/route.test.ts +86 -0
  7. package/app/api/sessions/[id]/mcp-auth/route.ts +58 -0
  8. package/app/api/shares/route.test.ts +3 -4
  9. package/app/api/workspaces/[id]/route.ts +9 -1
  10. package/app/api/worktree-sweep/route.test.ts +21 -0
  11. package/app/api/worktree-sweep/route.ts +24 -4
  12. package/components/Chat/ChatPanel.tsx +6 -3
  13. package/components/Chat/McpServers.tsx +61 -24
  14. package/components/Chat/McpSignIn.tsx +122 -0
  15. package/components/Chat/session.ts +8 -0
  16. package/components/Health/SweepRuns.tsx +1 -0
  17. package/components/Projects/ProjectSettingsDialog.tsx +7 -0
  18. package/components/Projects/ProjectTierField.tsx +39 -0
  19. package/components/Workspaces/ProjectTiersForm.tsx +33 -0
  20. package/components/Workspaces/WorkspaceSettings.tsx +8 -0
  21. package/data/chat/mcpAuth.ts +42 -0
  22. package/data/projects/queries.ts +4 -0
  23. package/data/workspaces/index.ts +6 -0
  24. package/lib/chat/driver.ts +24 -2
  25. package/lib/chat/drivers/claude-approvals.ts +7 -0
  26. package/lib/chat/drivers/claude-mcp-auth.ts +82 -0
  27. package/lib/chat/drivers/claude.ts +7 -0
  28. package/lib/chat/events.ts +12 -0
  29. package/lib/chat/mcp-auth.ts +193 -0
  30. package/lib/chat/registry.ts +2 -0
  31. package/lib/chat/runner.ts +18 -0
  32. package/lib/chat/worker/client.ts +35 -1
  33. package/lib/chat/worker/host.ts +9 -0
  34. package/lib/chat/worker/main.ts +1 -1
  35. package/lib/chat/worker/mcp-auth.ts +119 -0
  36. package/lib/chat/worker/protocol.ts +19 -0
  37. package/lib/db/migrations/20261018T1730_mcp_sign_ins.ts +22 -0
  38. package/lib/db/migrations/20261018T2100_orch_full_access.ts +16 -0
  39. package/lib/db/migrations/20261018T2130_project_tier.ts +38 -0
  40. package/lib/db/types.ts +7 -0
  41. package/lib/done/testing.ts +2 -2
  42. package/lib/env-setup.ts +8 -1
  43. package/lib/health/boots.ts +2 -8
  44. package/lib/orchestrator/armed.test-helpers.ts +7 -2
  45. package/lib/orchestrator/armed.ts +14 -1
  46. package/lib/orchestrator/brief.ts +23 -11
  47. package/lib/orchestrator/external-gates.ts +62 -1
  48. package/lib/orchestrator/fast-lane-state.ts +69 -0
  49. package/lib/orchestrator/fast-lane.ts +96 -0
  50. package/lib/orchestrator/full-access.ts +21 -0
  51. package/lib/orchestrator/gates.ts +8 -2
  52. package/lib/orchestrator/project-tier-tool.ts +33 -0
  53. package/lib/orchestrator/quick.ts +47 -4
  54. package/lib/orchestrator/review.ts +3 -0
  55. package/lib/orchestrator/serve.ts +17 -3
  56. package/lib/orchestrator/shell-guard.ts +23 -1
  57. package/lib/orchestrator/signoff.test-helpers.ts +6 -2
  58. package/lib/orchestrator/signoff.ts +36 -19
  59. package/lib/orchestrator/sweep-arm.ts +180 -0
  60. package/lib/orchestrator/tool-names.ts +5 -1
  61. package/lib/orchestrator/tool-schemas.ts +41 -2
  62. package/lib/orchestrator/tools.ts +8 -2
  63. package/lib/orchestrator/watcher.ts +5 -0
  64. package/lib/orchestrator/write-guard.ts +34 -7
  65. package/lib/project-config/brief.ts +3 -1
  66. package/lib/project-config/database.ts +6 -0
  67. package/lib/project-config/index.ts +108 -21
  68. package/lib/project-config/schema.ts +10 -4
  69. package/lib/project-config/session.ts +9 -6
  70. package/lib/project-tier/getProjectTier.ts +18 -0
  71. package/lib/project-tier/index.ts +3 -0
  72. package/lib/project-tier/setProjectTier.ts +14 -0
  73. package/lib/project-tier/types.ts +3 -0
  74. package/lib/security/net.ts +7 -4
  75. package/lib/start-options.ts +8 -1
  76. package/lib/tasks/finish.ts +2 -1
  77. package/lib/tasks/index.ts +10 -5
  78. package/lib/tasks/move-testing.ts +2 -2
  79. package/lib/tasks/state.ts +7 -1
  80. package/lib/test-temp.ts +30 -0
  81. package/lib/workspaces.ts +6 -2
  82. package/lib/worktree-sweep/caches.ts +6 -0
  83. package/lib/worktree-sweep/extras.ts +16 -8
  84. package/lib/worktree-sweep/report.ts +1 -0
  85. package/lib/worktree-sweep/settings.ts +2 -2
  86. package/lib/worktree-sweep/testTemp.ts +113 -0
  87. package/lib/worktree-sweep/types.ts +2 -0
  88. package/package.json +1 -1
  89. package/scripts/autodeploy.test.ts +2 -2
  90. package/scripts/check-bundle.test.ts +2 -2
  91. package/scripts/hooks/banned-terms.test.ts +3 -2
  92. package/scripts/hooks/staged-secrets.test.ts +2 -2
  93. package/scripts/lib/build-swap.test.ts +2 -3
  94. package/scripts/lib/wsl.test.ts +2 -3
  95. package/scripts/update-run.test.ts +2 -2
package/lib/workspaces.ts CHANGED
@@ -8,10 +8,11 @@ import { db, type Workspace } from "./db";
8
8
 
9
9
  type WorkspaceRow = Omit<
10
10
  Workspace,
11
- "collapsed" | "wake_tasks_on_pr" | "orch_mcp_servers"
11
+ "collapsed" | "wake_tasks_on_pr" | "orch_mcp_servers" | "orch_full_access"
12
12
  > & {
13
13
  collapsed: number;
14
14
  wake_tasks_on_pr: number;
15
+ orch_full_access: number;
15
16
  orch_mcp_servers: string;
16
17
  };
17
18
 
@@ -38,6 +39,7 @@ const toWorkspace = (row: WorkspaceRow): Workspace => ({
38
39
  ...row,
39
40
  collapsed: !!row.collapsed,
40
41
  wake_tasks_on_pr: !!row.wake_tasks_on_pr,
42
+ orch_full_access: !!row.orch_full_access,
41
43
  orch_mcp_servers: parseServers(row.orch_mcp_servers),
42
44
  });
43
45
 
@@ -86,6 +88,7 @@ export function updateWorkspace(
86
88
  sortOrder?: number;
87
89
  maxRunningTasks?: number | null;
88
90
  wakeTasksOnPR?: boolean;
91
+ orchFullAccess?: boolean;
89
92
  mcpServers?: string[];
90
93
  }
91
94
  ): Workspace | null {
@@ -93,13 +96,14 @@ export function updateWorkspace(
93
96
  if (!existing) return null;
94
97
  const name = updates.name?.trim() || existing.name;
95
98
  db.prepare(
96
- `UPDATE workspaces SET name = ?, collapsed = ?, sort_order = ?, max_running_tasks = ?, wake_tasks_on_pr = ?, orch_mcp_servers = ? WHERE id = ?`
99
+ `UPDATE workspaces SET name = ?, collapsed = ?, sort_order = ?, max_running_tasks = ?, wake_tasks_on_pr = ?, orch_full_access = ?, orch_mcp_servers = ? WHERE id = ?`
97
100
  ).run(
98
101
  name,
99
102
  (updates.collapsed ?? existing.collapsed) ? 1 : 0,
100
103
  updates.sortOrder ?? existing.sort_order,
101
104
  taskLimit(updates.maxRunningTasks, existing.max_running_tasks),
102
105
  (updates.wakeTasksOnPR ?? existing.wake_tasks_on_pr) ? 1 : 0,
106
+ (updates.orchFullAccess ?? existing.orch_full_access) ? 1 : 0,
103
107
  JSON.stringify(serverNames(updates.mcpServers, existing.orch_mcp_servers)),
104
108
  id
105
109
  );
@@ -10,6 +10,7 @@ import fs from "fs";
10
10
  import path from "path";
11
11
  import { execFile } from "child_process";
12
12
  import { SESSION_TMP_PREFIX, tmpName } from "../sessions/leftovers";
13
+ import { isTestTempName } from "./testTemp";
13
14
  import type { Candidate, SweepSkipped } from "./types";
14
15
 
15
16
  const DAY_MS = 24 * 3600_000;
@@ -92,6 +93,11 @@ export async function findCacheLeftovers(deps: CacheDeps): Promise<{
92
93
  const livePrefixes = [...deps.liveIds].map(tmpName);
93
94
  for (const dir of subdirs(deps.tmpRoot)) {
94
95
  const name = path.basename(dir);
96
+ if (isTestTempName(name)) {
97
+ if (deps.now - mtime(dir) >= DAY_MS)
98
+ await add(dir, "test-temp", "left behind by a test run");
99
+ continue;
100
+ }
95
101
  const isSession = name.startsWith(SESSION_TMP_PREFIX);
96
102
  if (!isSession && !name.startsWith("agentos-preview-")) continue;
97
103
  if (deps.now - mtime(dir) < DAY_MS) continue;
@@ -69,6 +69,18 @@ export async function cachedRootSize(
69
69
 
70
70
  export const forgetRootSizes = () => rootSizes.clear();
71
71
 
72
+ // Removes a folder directly inside one of the roots, and nothing else.
73
+ export async function dropFolderIn(
74
+ roots: string[],
75
+ folder: string
76
+ ): Promise<boolean> {
77
+ const target = path.resolve(folder);
78
+ if (!roots.some((r) => r && path.dirname(target) === path.resolve(r)))
79
+ return false;
80
+ await fs.promises.rm(target, { recursive: true, force: true });
81
+ return !fs.existsSync(target);
82
+ }
83
+
72
84
  export async function defaultExtras(
73
85
  opts: { fresh?: boolean } = {}
74
86
  ): Promise<Extras> {
@@ -108,14 +120,10 @@ export async function defaultExtras(
108
120
  count: dirCount(SPARE_ROOT),
109
121
  bytes: await cachedRootSize(SPARE_ROOT, size, !!opts.fresh),
110
122
  },
111
- drop: async (c) => {
112
- if (c.kind === "database") return dropTaskDatabase(c, froms);
113
- const target = path.resolve(c.path);
114
- if (!roots.some((r) => r && path.dirname(target) === path.resolve(r)))
115
- return false;
116
- await fs.promises.rm(target, { recursive: true, force: true });
117
- return !fs.existsSync(target);
118
- },
123
+ drop: async (c) =>
124
+ c.kind === "database"
125
+ ? dropTaskDatabase(c, froms)
126
+ : dropFolderIn(roots, c.path),
119
127
  };
120
128
  }
121
129
 
@@ -21,6 +21,7 @@ const TITLES: Record<SweepKind, string> = {
21
21
  database: "Task databases",
22
22
  spare: "Dependency spares",
23
23
  temp: "Temp folders",
24
+ "test-temp": "Test temp folders",
24
25
  "derived-data": "Xcode build folders of removed worktrees",
25
26
  };
26
27
 
@@ -83,8 +83,8 @@ export function projectSweepOverrides(): Array<{
83
83
  }
84
84
 
85
85
  // The full clean-up (test leftovers, task databases, spares, temp folders,
86
- // build caches) removes nothing until a person turns it on for this machine,
87
- // after reading what a dry run found.
86
+ // build caches) removes nothing until Saad turns it on for this machine, or
87
+ // the orchestrator does on his word after a dry run from the last hour.
88
88
  const ARMED_KEY = "worktree_sweep.reaper";
89
89
 
90
90
  export function reaperArmed(): boolean {
@@ -0,0 +1,113 @@
1
+ // Folders the test suite makes in the OS temp folder, by name. The sweep
2
+ // takes only these, never another aos-… or agent-os-… folder, so AgentOS's
3
+ // own temp folders (aos-review-, aos-rebase-…) and a person's are safe.
4
+ // lib/test-temp.test.ts fails when a test uses a prefix missing here.
5
+ export const TEST_TEMP_PREFIXES: readonly string[] = [
6
+ "agent-os-git-",
7
+ "agent-os-home-",
8
+ "agent-os-kept-",
9
+ "agent-os-limits-",
10
+ "agent-os-session-logs-",
11
+ "agent-os-test-",
12
+ "aos-acc-",
13
+ "aos-add-",
14
+ "aos-armed-repo-",
15
+ "aos-art-",
16
+ "aos-autodeploy-",
17
+ "aos-caches-",
18
+ "aos-channel-",
19
+ "aos-clone-",
20
+ "aos-config-",
21
+ "aos-crypt-",
22
+ "aos-db-",
23
+ "aos-db-drops-",
24
+ "aos-db-wt-",
25
+ "aos-decl-",
26
+ "aos-device-keys-",
27
+ "aos-done-home-",
28
+ "aos-done-repo-",
29
+ "aos-env-",
30
+ "aos-fast-",
31
+ "aos-files-",
32
+ "aos-git-",
33
+ "aos-gr-",
34
+ "aos-heavy-",
35
+ "aos-init-route-",
36
+ "aos-install-",
37
+ "aos-kind-",
38
+ "aos-launch-plain-",
39
+ "aos-launch-repo-",
40
+ "aos-limits-",
41
+ "aos-mcp-",
42
+ "aos-merge-policy-",
43
+ "aos-mig-",
44
+ "aos-move-",
45
+ "aos-no-approvals-",
46
+ "aos-nogit-",
47
+ "aos-notify-",
48
+ "aos-orch-act-",
49
+ "aos-orch-add-",
50
+ "aos-orch-approve-",
51
+ "aos-orch-armed-",
52
+ "aos-orch-asks-",
53
+ "aos-orch-brakes-",
54
+ "aos-orch-ext-repo-",
55
+ "aos-orch-external-",
56
+ "aos-orch-home-",
57
+ "aos-orch-move-",
58
+ "aos-orch-plain-",
59
+ "aos-orch-questions-",
60
+ "aos-orch-recipes-",
61
+ "aos-orch-remind-",
62
+ "aos-orch-repo-",
63
+ "aos-orch-rules-",
64
+ "aos-orch-settle-",
65
+ "aos-orch-signoff-",
66
+ "aos-orch-sweep-",
67
+ "aos-origin-",
68
+ "aos-path-",
69
+ "aos-plain-",
70
+ "aos-plan-",
71
+ "aos-ports-",
72
+ "aos-pr-",
73
+ "aos-pr-async-",
74
+ "aos-pr-create-",
75
+ "aos-pr-gen-",
76
+ "aos-pr-route-",
77
+ "aos-prompts-",
78
+ "aos-quick-",
79
+ "aos-quick-repo-",
80
+ "aos-relay-",
81
+ "aos-relay-none-",
82
+ "aos-report-",
83
+ "aos-resurrect-claude-",
84
+ "aos-resurrect-home-",
85
+ "aos-run-",
86
+ "aos-runner-",
87
+ "aos-sec-",
88
+ "aos-shot-",
89
+ "aos-sim-",
90
+ "aos-spare-",
91
+ "aos-stack-",
92
+ "aos-start-opts-",
93
+ "aos-start-plain-",
94
+ "aos-sweep-own-",
95
+ "aos-t-",
96
+ "aos-task-diff-",
97
+ "aos-temp-helper-",
98
+ "aos-tmux-",
99
+ "aos-transcripts-",
100
+ "aos-ui-",
101
+ "aos-upd-",
102
+ "aos-usage-",
103
+ "aos-wg-",
104
+ "aos-wg-path-",
105
+ "aos-wl-",
106
+ "aos-worker-env-",
107
+ "aos-worker-tmux-",
108
+ "aos-wsl-",
109
+ "aos-wt-",
110
+ ];
111
+
112
+ export const isTestTempName = (name: string) =>
113
+ TEST_TEMP_PREFIXES.some((p) => name.startsWith(p));
@@ -6,6 +6,7 @@ export type SweepKind =
6
6
  | "database"
7
7
  | "spare"
8
8
  | "temp"
9
+ | "test-temp"
9
10
  | "derived-data";
10
11
 
11
12
  // Kinds that go only once the full clean-up is turned on.
@@ -14,6 +15,7 @@ export const ARMED_KINDS: ReadonlySet<SweepKind> = new Set([
14
15
  "database",
15
16
  "spare",
16
17
  "temp",
18
+ "test-temp",
17
19
  "derived-data",
18
20
  ]);
19
21
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@saadnvd1/agent-os",
3
- "version": "0.3.664",
3
+ "version": "0.3.672",
4
4
  "description": "Self-hosted web UI for managing Claude Code sessions",
5
5
  "bin": {
6
6
  "agent-os": "scripts/agent-os"
@@ -1,8 +1,8 @@
1
1
  import { execFileSync } from "child_process";
2
2
  import * as fs from "fs";
3
- import * as os from "os";
4
3
  import * as path from "path";
5
4
  import { describe, expect, it } from "vitest";
5
+ import { tempDir } from "@/lib/test-temp";
6
6
 
7
7
  // Runs the real scripts/autodeploy in a scratch clone with a stub redeploy
8
8
  // (a pull) and wait-quiet, against a scratch origin and HOME.
@@ -11,7 +11,7 @@ const sh = (cwd: string, ...args: string[]) =>
11
11
  execFileSync("git", args, { cwd, encoding: "utf-8" }).trim();
12
12
 
13
13
  function setup() {
14
- const root = fs.mkdtempSync(path.join(os.tmpdir(), "aos-autodeploy-"));
14
+ const root = tempDir("aos-autodeploy-");
15
15
  const origin = path.join(root, "origin.git");
16
16
  const work = path.join(root, "work");
17
17
  const clone = path.join(root, "live");
@@ -1,13 +1,13 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
  import { spawnSync } from "child_process";
3
3
  import fs from "fs";
4
- import os from "os";
5
4
  import path from "path";
5
+ import { tempDir } from "@/lib/test-temp";
6
6
 
7
7
  const script = path.join(__dirname, "check-bundle.mjs");
8
8
 
9
9
  function run(stats: unknown) {
10
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), "bundle-"));
10
+ const dir = tempDir("bundle-");
11
11
  const file = path.join(dir, "stats.json");
12
12
  fs.writeFileSync(file, JSON.stringify(stats));
13
13
  const r = spawnSync(process.execPath, [script, file], { encoding: "utf-8" });
@@ -4,11 +4,12 @@ import fs from "fs";
4
4
  import os from "os";
5
5
  import path from "path";
6
6
  import { GENERIC_TERMS, loadTerms, matcher, scanTree } from "./banned-terms";
7
+ import { tempDir } from "@/lib/test-temp";
7
8
 
8
9
  const generic = GENERIC_TERMS[0];
9
10
 
10
11
  function tempRepo() {
11
- const repo = fs.mkdtempSync(path.join(os.tmpdir(), "banned-terms-"));
12
+ const repo = tempDir("banned-terms-");
12
13
  const git = (...args: string[]) =>
13
14
  execFileSync("git", args, { cwd: repo, stdio: "pipe" });
14
15
  git("init", "-q");
@@ -22,7 +23,7 @@ describe("loadTerms", () => {
22
23
  });
23
24
 
24
25
  it("adds the private regexes, skipping blanks and comments", () => {
25
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), "banned-names-"));
26
+ const dir = tempDir("banned-names-");
26
27
  const file = path.join(dir, "banned-names");
27
28
  fs.writeFileSync(file, "# names\nacme[ -]?widgets\n\n zorp \n");
28
29
  try {
@@ -1,9 +1,9 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
  import { execFileSync, spawnSync } from "child_process";
3
3
  import fs from "fs";
4
- import os from "os";
5
4
  import path from "path";
6
5
  import { scanDiff } from "./staged-secrets";
6
+ import { tempDir } from "@/lib/test-temp";
7
7
 
8
8
  // Fakes are built here, never written whole: a literal would be the very
9
9
  // thing this scan (and the merge gate) refuses.
@@ -62,7 +62,7 @@ describe("scanDiff", () => {
62
62
  });
63
63
 
64
64
  it("fails the commit on a staged secret, and passes once it's gone", () => {
65
- const repo = fs.mkdtempSync(path.join(os.tmpdir(), "staged-secrets-"));
65
+ const repo = tempDir("staged-secrets-");
66
66
  const git = (...args: string[]) =>
67
67
  execFileSync("git", args, { cwd: repo, stdio: "pipe" });
68
68
  const scan = () =>
@@ -2,15 +2,14 @@ import { spawnSync } from "node:child_process";
2
2
  import {
3
3
  existsSync,
4
4
  mkdirSync,
5
- mkdtempSync,
6
5
  readFileSync,
7
6
  rmSync,
8
7
  utimesSync,
9
8
  writeFileSync,
10
9
  } from "node:fs";
11
- import { tmpdir } from "node:os";
12
10
  import { join } from "node:path";
13
11
  import { afterEach, beforeEach, describe, expect, it } from "vitest";
12
+ import { tempDir } from "@/lib/test-temp";
14
13
 
15
14
  const LIB = join(import.meta.dirname, "build-swap.sh");
16
15
 
@@ -41,7 +40,7 @@ const BUILD = `
41
40
  `;
42
41
 
43
42
  beforeEach(() => {
44
- dir = mkdtempSync(join(tmpdir(), "build-swap-"));
43
+ dir = tempDir("build-swap-");
45
44
  mkdirSync(join(dir, ".next/cache/turbopack"), { recursive: true });
46
45
  writeFileSync(join(dir, ".next/BUILD_ID"), "old\n");
47
46
  writeFileSync(join(dir, ".next/cache/turbopack/db"), "cache\n");
@@ -3,15 +3,14 @@ import {
3
3
  chmodSync,
4
4
  existsSync,
5
5
  mkdirSync,
6
- mkdtempSync,
7
6
  readFileSync,
8
7
  rmSync,
9
8
  symlinkSync,
10
9
  writeFileSync,
11
10
  } from "node:fs";
12
- import { tmpdir } from "node:os";
13
11
  import { join } from "node:path";
14
12
  import { afterEach, beforeEach, describe, expect, it } from "vitest";
13
+ import { tempDir } from "@/lib/test-temp";
15
14
 
16
15
  // The installer's WSL paths, run on any OS: `uname`, `ps`, the browser
17
16
  // openers and the package managers are fakes, and the kernel release is a
@@ -60,7 +59,7 @@ function run(
60
59
  }
61
60
 
62
61
  beforeEach(() => {
63
- dir = mkdtempSync(join(tmpdir(), "aos-wsl-"));
62
+ dir = tempDir("aos-wsl-");
64
63
  mkdirSync(join(dir, "bin"));
65
64
  for (const tool of TOOLS) {
66
65
  const real = ["/usr/bin", "/bin"]
@@ -1,16 +1,16 @@
1
1
  import { execFile } from "child_process";
2
2
  import fs from "fs";
3
- import os from "os";
4
3
  import path from "path";
5
4
  import { promisify } from "util";
6
5
  import { afterEach, beforeEach, describe, expect, it } from "vitest";
6
+ import { tempDir } from "@/lib/test-temp";
7
7
 
8
8
  const exec = promisify(execFile);
9
9
  const runner = path.join(__dirname, "update-run.mjs");
10
10
 
11
11
  let dir: string;
12
12
  beforeEach(() => {
13
- dir = fs.mkdtempSync(path.join(os.tmpdir(), "aos-runner-"));
13
+ dir = tempDir("aos-runner-");
14
14
  fs.writeFileSync(
15
15
  path.join(dir, "state.json"),
16
16
  JSON.stringify({ id: "r1", phase: "starting", from: "a" })