@minhpnq1807/contextos 0.5.34 → 0.5.35
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/CHANGELOG.md +7 -0
- package/bin/ctx.js +46 -14
- package/package.json +1 -1
- package/plugins/ctx/.codex-plugin/plugin.json +1 -1
- package/plugins/ctx/lib/copilot-hooks.js +58 -0
- package/plugins/ctx/lib/copilot-mcp.js +43 -0
- package/plugins/ctx/lib/ruler-sync.js +26 -3
- package/plugins/ctx/lib/setup-wizard.js +2 -1
- package/plugins/ctx/lib/skillshare-sync.js +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.35
|
|
4
|
+
|
|
5
|
+
- **Add GitHub Copilot agent support:** New `copilot` agent for `ctx install --agent copilot` and `ctx setup`. Creates `.github/copilot-instructions.md` with ContextOS integration marker and configures `ctx-mcp` MCP server in `.vscode/mcp.json`. Copilot is now recognized by Ruler (`ctx sync --rules`) and Skillshare (`ctx sync --skills`) alongside existing codex, claude, and agy agents.
|
|
6
|
+
- **Agent selection defaults to none:** `ctx setup` and `ctx install` no longer pre-select all agents. Users must explicitly choose which agents to install via the interactive multiSelect prompt or `--agents` flag. This prevents accidental installation of unwanted agent hooks.
|
|
7
|
+
- **copilot-hooks.js:** Writes a managed `copilot-instructions.md` file under `.github/`, appending to existing content if present. Uses a marker comment (`<!-- managed by ContextOS -->`) to avoid duplicate sections.
|
|
8
|
+
- **copilot-mcp.js:** Configures `ctx-mcp` server in `.vscode/mcp.json` using the same pattern as existing claude/antigravity MCP modules.
|
|
9
|
+
|
|
3
10
|
## 0.5.34
|
|
4
11
|
|
|
5
12
|
- **Real-time streaming output during install/setup:** Replaced `captureSetupOutput` (buffered) with `streamSetupOutput` — now prints each line immediately with `│ ` prefix as it arrives, eliminating the perceived "hang" during long-running downloads and installs.
|
package/bin/ctx.js
CHANGED
|
@@ -24,6 +24,8 @@ import { installClaudeHooks } from "../plugins/ctx/lib/claude-hooks.js";
|
|
|
24
24
|
import { installClaudeMcp } from "../plugins/ctx/lib/claude-mcp.js";
|
|
25
25
|
import { installAntigravityHooks } from "../plugins/ctx/lib/antigravity-hooks.js";
|
|
26
26
|
import { installAntigravityMcp } from "../plugins/ctx/lib/antigravity-mcp.js";
|
|
27
|
+
import { installCopilotHooks } from "../plugins/ctx/lib/copilot-hooks.js";
|
|
28
|
+
import { installCopilotMcp } from "../plugins/ctx/lib/copilot-mcp.js";
|
|
27
29
|
import { syncRules } from "../plugins/ctx/lib/ruler-sync.js";
|
|
28
30
|
import { writeInnerGitignore, ensureRootGitignore } from "../plugins/ctx/lib/gitignore.js";
|
|
29
31
|
import { syncSkills } from "../plugins/ctx/lib/skillshare-sync.js";
|
|
@@ -42,11 +44,11 @@ function usage() {
|
|
|
42
44
|
|
|
43
45
|
Usage:
|
|
44
46
|
ctx install Interactive multi-select agent installer
|
|
45
|
-
ctx install --agent <name> Install a specific agent (codex|claude|agy)
|
|
47
|
+
ctx install --agent <name> Install a specific agent (codex|claude|agy|copilot)
|
|
46
48
|
ctx install --copy Legacy: copy plugin folder only (no hooks/mcp)
|
|
47
49
|
ctx setup Interactive full setup wizard
|
|
48
50
|
ctx setup --yes Auto-confirm all setup prompts
|
|
49
|
-
ctx setup --agents codex,claude,agy
|
|
51
|
+
ctx setup --agents codex,claude,agy,copilot Pre-select agents to install
|
|
50
52
|
ctx setup --no-rules Skip AGENTS.md rule sync
|
|
51
53
|
ctx setup --no-skills Skip skill sync
|
|
52
54
|
ctx setup --quiet Quiet mode (minimal output)
|
|
@@ -56,17 +58,17 @@ Usage:
|
|
|
56
58
|
ctx stats Show workspace statistics
|
|
57
59
|
ctx benchmark -- "task" Benchmark workspace for a task
|
|
58
60
|
ctx sync --rules Sync AGENTS.md rules to all agents
|
|
59
|
-
ctx sync --rules --agents codex,claude,agy Sync rules to specific agents only
|
|
61
|
+
ctx sync --rules --agents codex,claude,agy,copilot Sync rules to specific agents only
|
|
60
62
|
ctx sync --rules --dry-run Preview rule sync without writing
|
|
61
63
|
ctx sync --rules --no-import-codex-mcp Skip importing Codex MCP servers
|
|
62
64
|
ctx sync --skills Sync skills across agents
|
|
63
|
-
ctx sync --skills --agents codex,claude,agy Sync skills to specific agents only
|
|
65
|
+
ctx sync --skills --agents codex,claude,agy,copilot Sync skills to specific agents only
|
|
64
66
|
ctx sync --skills --dry-run Preview skill sync without writing
|
|
65
67
|
ctx sync --skills --no-collect Skip collecting new skills
|
|
66
68
|
ctx sync --skills --no-embeddings Skip embedding generation
|
|
67
69
|
ctx sync --skills --verbose Verbose skill sync output
|
|
68
70
|
ctx sync --workflows Sync workflows across agents
|
|
69
|
-
ctx sync --workflows --agents codex,claude,agy Sync workflows to specific agents
|
|
71
|
+
ctx sync --workflows --agents codex,claude,agy,copilot Sync workflows to specific agents
|
|
70
72
|
ctx sync --workflows --dry-run Preview workflow sync without writing
|
|
71
73
|
ctx embeddings warm -- "task" Pre-warm embedding caches for a task
|
|
72
74
|
ctx ruler -- <ruler args> Passthrough to ruler CLI
|
|
@@ -76,9 +78,10 @@ Usage:
|
|
|
76
78
|
}
|
|
77
79
|
|
|
78
80
|
const SUPPORTED_AGENTS = [
|
|
79
|
-
{ label: "Codex",
|
|
80
|
-
{ label: "Claude Code",
|
|
81
|
-
{ label: "Antigravity (agy)",
|
|
81
|
+
{ label: "Codex", value: "codex", selected: false },
|
|
82
|
+
{ label: "Claude Code", value: "claude", selected: false },
|
|
83
|
+
{ label: "Antigravity (agy)", value: "agy", selected: false },
|
|
84
|
+
{ label: "GitHub Copilot", value: "copilot", selected: false }
|
|
82
85
|
];
|
|
83
86
|
|
|
84
87
|
function normalizeInstallAgent(agent) {
|
|
@@ -90,8 +93,9 @@ function normalizeInstallAgent(agent) {
|
|
|
90
93
|
" ctx install --agent codex",
|
|
91
94
|
" ctx install --agent claude",
|
|
92
95
|
" ctx install --agent agy",
|
|
96
|
+
" ctx install --agent copilot",
|
|
93
97
|
"",
|
|
94
|
-
"Do not run `ctx install --agent codex|claude|agy`: `|` is a shell pipe."
|
|
98
|
+
"Do not run `ctx install --agent codex|claude|agy|copilot`: `|` is a shell pipe."
|
|
95
99
|
].join("\n"));
|
|
96
100
|
}
|
|
97
101
|
if (normalized === "antigravity") return "agy";
|
|
@@ -271,8 +275,35 @@ async function install({ copy = false, agent = "codex" } = {}) {
|
|
|
271
275
|
return;
|
|
272
276
|
}
|
|
273
277
|
|
|
278
|
+
if (agent === "copilot") {
|
|
279
|
+
progress.step(10, "copying package");
|
|
280
|
+
const installRoot = copyPackageRoot({ rootDir, targetRoot: agentInstallRoot("copilot") });
|
|
281
|
+
progress.step(25, "installing hooks");
|
|
282
|
+
const hooksPath = installCopilotHooks({ cwd: process.cwd(), installRoot });
|
|
283
|
+
progress.step(40, "installing mcp");
|
|
284
|
+
const mcpConfigPath = installCopilotMcp({ cwd: process.cwd(), installRoot });
|
|
285
|
+
progress.step(50, "configuring gitignore");
|
|
286
|
+
writeInnerGitignore(installRoot);
|
|
287
|
+
ensureRootGitignore(process.cwd());
|
|
288
|
+
progress.step(55, "warming embeddings");
|
|
289
|
+
const warmResult = await warmInstallEmbeddings();
|
|
290
|
+
progress.done("copilot installed");
|
|
291
|
+
console.log("Installed ctx hooks for GitHub Copilot.");
|
|
292
|
+
console.log(`Stable install root: ${installRoot}`);
|
|
293
|
+
console.log(`Installed ContextOS instructions to ${hooksPath}`);
|
|
294
|
+
console.log(`Installed ctx-mcp MCP server to ${mcpConfigPath}`);
|
|
295
|
+
console.log(`Embedding model cache: ${modelCacheDir(contextOSDataDir())}`);
|
|
296
|
+
console.log(`Embedding vectors cache: ${warmResult.cachePath}`);
|
|
297
|
+
console.log(`File path embeddings warmed: ${warmResult.fileCount || 0}`);
|
|
298
|
+
console.log(`Skill embeddings warmed: ${warmResult.skillCount || 0}`);
|
|
299
|
+
console.log(`Workflow embeddings warmed: ${warmResult.workflowCount || 0}`);
|
|
300
|
+
console.log(`Prompt context injection: ${inject ? "enabled" : "quiet logging only"}`);
|
|
301
|
+
console.log("Restart VS Code or Copilot if it was already running, then submit a task to trigger ContextOS.");
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
274
305
|
if (agent !== "codex") {
|
|
275
|
-
throw new Error(`Unknown agent '${agent}'. Expected codex, claude, or
|
|
306
|
+
throw new Error(`Unknown agent '${agent}'. Expected codex, claude, agy, or copilot.`);
|
|
276
307
|
}
|
|
277
308
|
|
|
278
309
|
progress.step(10, "copying marketplace");
|
|
@@ -547,9 +578,10 @@ async function setup({ args = [], cwd = process.cwd() } = {}) {
|
|
|
547
578
|
const selected = await multiSelect({
|
|
548
579
|
message: "Select agents to install:",
|
|
549
580
|
options: [
|
|
550
|
-
{ label: "Codex", value: "codex",
|
|
551
|
-
{ label: "Claude", value: "claude",
|
|
552
|
-
{ label: "Antigravity (agy)", value: "agy",
|
|
581
|
+
{ label: "Codex", value: "codex", selected: options.agents.includes("codex") },
|
|
582
|
+
{ label: "Claude", value: "claude", selected: options.agents.includes("claude") },
|
|
583
|
+
{ label: "Antigravity (agy)", value: "agy", selected: options.agents.includes("agy") },
|
|
584
|
+
{ label: "GitHub Copilot", value: "copilot", selected: options.agents.includes("copilot") }
|
|
553
585
|
]
|
|
554
586
|
});
|
|
555
587
|
options.agents = selected;
|
|
@@ -576,7 +608,7 @@ async function setup({ args = [], cwd = process.cwd() } = {}) {
|
|
|
576
608
|
for (const line of setupSummaryLines({ cwd, ...options })) console.log(`│ ${line}`);
|
|
577
609
|
console.log("");
|
|
578
610
|
|
|
579
|
-
if (!options.agents.length) throw new Error("No agents selected. Use --agents codex,claude,agy.");
|
|
611
|
+
if (!options.agents.length) throw new Error("No agents selected. Use --agents codex,claude,agy,copilot.");
|
|
580
612
|
|
|
581
613
|
for (const agent of options.agents) {
|
|
582
614
|
console.log(`◇ Setting up ${agent}...`);
|
package/package.json
CHANGED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Copilot reads instructions from:
|
|
6
|
+
* 1. .github/copilot-instructions.md (repo-level)
|
|
7
|
+
* 2. AGENTS.md files (nearest in directory tree)
|
|
8
|
+
*
|
|
9
|
+
* Since ContextOS already syncs AGENTS.md through Ruler,
|
|
10
|
+
* this module writes a copilot-instructions.md that signals
|
|
11
|
+
* ContextOS integration and points Copilot at the ctx-mcp MCP server.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const MARKER = "<!-- managed by ContextOS -->";
|
|
15
|
+
|
|
16
|
+
function buildCopilotInstructions({ installRoot } = {}) {
|
|
17
|
+
return [
|
|
18
|
+
MARKER,
|
|
19
|
+
"# ContextOS Integration",
|
|
20
|
+
"",
|
|
21
|
+
"This project uses [ContextOS](https://github.com/khovan123/contextOS) for task-aware context injection.",
|
|
22
|
+
"",
|
|
23
|
+
"## MCP Server",
|
|
24
|
+
"",
|
|
25
|
+
"The `ctx-mcp` MCP server is configured in `.vscode/mcp.json`.",
|
|
26
|
+
"It provides semantic file search, skill discovery, and rule scoring for this workspace.",
|
|
27
|
+
"",
|
|
28
|
+
"## Rules",
|
|
29
|
+
"",
|
|
30
|
+
"Project rules are defined in `AGENTS.md` files managed by Ruler.",
|
|
31
|
+
"These rules are automatically injected into your prompt context.",
|
|
32
|
+
""
|
|
33
|
+
].join("\n");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function copilotInstructionsPath(cwd = process.cwd()) {
|
|
37
|
+
return path.join(cwd, ".github", "copilot-instructions.md");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function installCopilotHooks({ cwd = process.cwd(), installRoot } = {}) {
|
|
41
|
+
const instructionsPath = copilotInstructionsPath(cwd);
|
|
42
|
+
const dir = path.dirname(instructionsPath);
|
|
43
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
44
|
+
|
|
45
|
+
// If the file exists and wasn't created by us, don't overwrite
|
|
46
|
+
if (fs.existsSync(instructionsPath)) {
|
|
47
|
+
const existing = fs.readFileSync(instructionsPath, "utf8");
|
|
48
|
+
if (!existing.includes(MARKER)) {
|
|
49
|
+
// Append our section
|
|
50
|
+
const content = existing.trimEnd() + "\n\n" + buildCopilotInstructions({ installRoot });
|
|
51
|
+
fs.writeFileSync(instructionsPath, content, "utf8");
|
|
52
|
+
return instructionsPath;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
fs.writeFileSync(instructionsPath, buildCopilotInstructions({ installRoot }), "utf8");
|
|
57
|
+
return instructionsPath;
|
|
58
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Copilot MCP configuration lives at .vscode/mcp.json (workspace-level).
|
|
6
|
+
* This is the standard location for VS Code / GitHub Copilot agent mode.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
function readJsonFile(filePath, fallback) {
|
|
10
|
+
if (!fs.existsSync(filePath)) return fallback;
|
|
11
|
+
const raw = fs.readFileSync(filePath, "utf8").trim();
|
|
12
|
+
if (!raw) return fallback;
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(raw);
|
|
15
|
+
} catch {
|
|
16
|
+
console.warn(`[ctx] warning: corrupt JSON in ${filePath}, overwriting with defaults`);
|
|
17
|
+
return fallback;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function copilotMcpConfigPath(cwd = process.cwd()) {
|
|
22
|
+
return path.join(cwd, ".vscode", "mcp.json");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function buildCopilotMcpConfig(existingConfig, { installRoot } = {}) {
|
|
26
|
+
const config = existingConfig && typeof existingConfig === "object" ? structuredClone(existingConfig) : {};
|
|
27
|
+
if (!config.mcpServers || typeof config.mcpServers !== "object") config.mcpServers = {};
|
|
28
|
+
config.mcpServers["ctx-mcp"] = {
|
|
29
|
+
type: "stdio",
|
|
30
|
+
command: "node",
|
|
31
|
+
args: [path.join(installRoot, "plugins", "ctx", "mcp", "server.js")]
|
|
32
|
+
};
|
|
33
|
+
return config;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function installCopilotMcp({ cwd = process.cwd(), configPath, installRoot } = {}) {
|
|
37
|
+
const mcpPath = configPath || copilotMcpConfigPath(cwd);
|
|
38
|
+
const existing = readJsonFile(mcpPath, {});
|
|
39
|
+
const next = buildCopilotMcpConfig(existing, { installRoot });
|
|
40
|
+
fs.mkdirSync(path.dirname(mcpPath), { recursive: true });
|
|
41
|
+
fs.writeFileSync(mcpPath, `${JSON.stringify(next, null, 2)}\n`, "utf8");
|
|
42
|
+
return mcpPath;
|
|
43
|
+
}
|
|
@@ -7,7 +7,7 @@ import { execFileSync, spawn } from "node:child_process";
|
|
|
7
7
|
|
|
8
8
|
import { defaultDataRoot } from "./workspace-data.js";
|
|
9
9
|
|
|
10
|
-
const DEFAULT_AGENTS = ["codex", "claude", "antigravity"];
|
|
10
|
+
const DEFAULT_AGENTS = ["codex", "claude", "antigravity", "copilot"];
|
|
11
11
|
const CTX_MCP_NAME = "ctx-mcp";
|
|
12
12
|
const CONTEXTOS_PROXY_MARKER = "/contextos/plugins/ctx/mcp/proxy.js";
|
|
13
13
|
const MCP_SERVER_RELATIVE = path.join("plugins", "ctx", "mcp", "server.js");
|
|
@@ -15,7 +15,8 @@ const AGENT_ALIASES = new Map([
|
|
|
15
15
|
["agy", "antigravity"],
|
|
16
16
|
["antigravity", "antigravity"],
|
|
17
17
|
["codex", "codex"],
|
|
18
|
-
["claude", "claude"]
|
|
18
|
+
["claude", "claude"],
|
|
19
|
+
["copilot", "copilot"]
|
|
19
20
|
]);
|
|
20
21
|
|
|
21
22
|
function statusLine(label, value) {
|
|
@@ -508,6 +509,10 @@ export function verifySync({ cwd = process.cwd(), agents = DEFAULT_AGENTS } = {}
|
|
|
508
509
|
path.join(cwd, ".gemini", "mcp.json"),
|
|
509
510
|
...antigravityMcpConfigPaths(),
|
|
510
511
|
path.join(cwd, "AGENTS.md")
|
|
512
|
+
],
|
|
513
|
+
copilot: [
|
|
514
|
+
path.join(cwd, ".vscode", "mcp.json"),
|
|
515
|
+
path.join(cwd, ".github", "copilot-instructions.md")
|
|
511
516
|
]
|
|
512
517
|
};
|
|
513
518
|
|
|
@@ -526,6 +531,7 @@ function resolveStableMcpServerPath(rootDir) {
|
|
|
526
531
|
path.join(codexRoot, MCP_SERVER_RELATIVE),
|
|
527
532
|
path.join(dataRoot, "agents", "claude", "contextos", MCP_SERVER_RELATIVE),
|
|
528
533
|
path.join(dataRoot, "agents", "agy", "contextos", MCP_SERVER_RELATIVE),
|
|
534
|
+
path.join(dataRoot, "agents", "copilot", "contextos", MCP_SERVER_RELATIVE),
|
|
529
535
|
path.join(rootDir, MCP_SERVER_RELATIVE)
|
|
530
536
|
];
|
|
531
537
|
for (const candidate of candidates) {
|
|
@@ -542,7 +548,7 @@ export async function syncRules({
|
|
|
542
548
|
logger = console.log
|
|
543
549
|
} = {}) {
|
|
544
550
|
const options = parseSyncRulesArgs(args);
|
|
545
|
-
if (!options.rules) throw new Error("Usage: ctx sync --rules [--agents codex,claude,antigravity] [--dry-run] [--force]");
|
|
551
|
+
if (!options.rules) throw new Error("Usage: ctx sync --rules [--agents codex,claude,antigravity,copilot] [--dry-run] [--force]");
|
|
546
552
|
|
|
547
553
|
logger("");
|
|
548
554
|
const ruler = checkRulerInstalled({ run });
|
|
@@ -608,6 +614,23 @@ export async function syncRules({
|
|
|
608
614
|
logger(statusLine("Syncing Antigravity MCP config...", antigravityMcp.servers.length ? `✓ ${antigravityMcp.servers.join(", ")}` : "none found"));
|
|
609
615
|
}
|
|
610
616
|
|
|
617
|
+
let copilotMcp = { changed: false };
|
|
618
|
+
if (options.agents.includes("copilot")) {
|
|
619
|
+
// Copilot MCP is managed by copilot-mcp.js during install,
|
|
620
|
+
// but we verify it's still in place during sync.
|
|
621
|
+
const vscodeMcpPath = path.join(cwd, ".vscode", "mcp.json");
|
|
622
|
+
if (fs.existsSync(vscodeMcpPath)) {
|
|
623
|
+
try {
|
|
624
|
+
const content = JSON.parse(fs.readFileSync(vscodeMcpPath, "utf8"));
|
|
625
|
+
copilotMcp.changed = Boolean(content?.mcpServers?.[CTX_MCP_NAME]);
|
|
626
|
+
logger(statusLine("Verifying Copilot MCP config...", copilotMcp.changed ? "✓ ctx-mcp found" : "not configured"));
|
|
627
|
+
} catch {
|
|
628
|
+
logger(statusLine("Verifying Copilot MCP config...", "⚠ parse error"));
|
|
629
|
+
}
|
|
630
|
+
} else {
|
|
631
|
+
logger(statusLine("Verifying Copilot MCP config...", "not installed (run ctx install --agent copilot)"));
|
|
632
|
+
}
|
|
633
|
+
}
|
|
611
634
|
logger("[ctx] Verifying sync...");
|
|
612
635
|
const checks = options.dryRun ? options.agents.map((agent) => ({ agent, ok: true, filePath: "(dry-run)" })) : verifySync({ cwd, agents: options.agents });
|
|
613
636
|
for (const check of checks) {
|
|
@@ -5,14 +5,15 @@ import readline from "node:readline/promises";
|
|
|
5
5
|
import { stdin as input, stdout as output } from "node:process";
|
|
6
6
|
import { execFileSync, execSync, spawn } from "node:child_process";
|
|
7
7
|
|
|
8
|
-
const DEFAULT_AGENTS = ["codex", "claude", "antigravity"];
|
|
8
|
+
const DEFAULT_AGENTS = ["codex", "claude", "antigravity", "copilot"];
|
|
9
9
|
const INSTALL_SH_URL = "https://raw.githubusercontent.com/runkids/skillshare/main/install.sh";
|
|
10
10
|
const INSTALL_PS_URL = "https://raw.githubusercontent.com/runkids/skillshare/main/install.ps1";
|
|
11
11
|
const AGENT_ALIASES = new Map([
|
|
12
12
|
["agy", "antigravity"],
|
|
13
13
|
["antigravity", "antigravity"],
|
|
14
14
|
["codex", "codex"],
|
|
15
|
-
["claude", "claude"]
|
|
15
|
+
["claude", "claude"],
|
|
16
|
+
["copilot", "copilot"]
|
|
16
17
|
]);
|
|
17
18
|
|
|
18
19
|
function statusLine(label, value) {
|