@tarcisiopgs/lisa 1.0.2 → 1.1.0
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/dist/index.js +93 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1638,7 +1638,7 @@ var TrelloSource = class {
|
|
|
1638
1638
|
name = "trello";
|
|
1639
1639
|
async fetchNextIssue(config2) {
|
|
1640
1640
|
const board = await findBoardByName(config2.team);
|
|
1641
|
-
const list = await findListByName(board.id, config2.
|
|
1641
|
+
const list = await findListByName(board.id, config2.pick_from);
|
|
1642
1642
|
const label = await findLabelByName(board.id, config2.label);
|
|
1643
1643
|
const cards = await trelloGet(
|
|
1644
1644
|
`/lists/${list.id}/cards`,
|
|
@@ -1715,6 +1715,50 @@ function createSource(name) {
|
|
|
1715
1715
|
return factory();
|
|
1716
1716
|
}
|
|
1717
1717
|
|
|
1718
|
+
// src/terminal.ts
|
|
1719
|
+
var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
1720
|
+
var SPINNER_INTERVAL_MS = 80;
|
|
1721
|
+
var spinnerTimer = null;
|
|
1722
|
+
var spinnerFrame = 0;
|
|
1723
|
+
function isTTY() {
|
|
1724
|
+
return process.stdout.isTTY === true;
|
|
1725
|
+
}
|
|
1726
|
+
function writeOSC(title) {
|
|
1727
|
+
process.stdout.write(`\x1B]0;${title}\x07`);
|
|
1728
|
+
}
|
|
1729
|
+
function setTitle(title) {
|
|
1730
|
+
if (!isTTY()) return;
|
|
1731
|
+
writeOSC(title);
|
|
1732
|
+
}
|
|
1733
|
+
function startSpinner(message) {
|
|
1734
|
+
if (!isTTY()) return;
|
|
1735
|
+
stopSpinner();
|
|
1736
|
+
spinnerFrame = 0;
|
|
1737
|
+
writeOSC(`${SPINNER_FRAMES[0]} Lisa \u2014 ${message}`);
|
|
1738
|
+
spinnerTimer = setInterval(() => {
|
|
1739
|
+
spinnerFrame = (spinnerFrame + 1) % SPINNER_FRAMES.length;
|
|
1740
|
+
writeOSC(`${SPINNER_FRAMES[spinnerFrame]} Lisa \u2014 ${message}`);
|
|
1741
|
+
}, SPINNER_INTERVAL_MS);
|
|
1742
|
+
}
|
|
1743
|
+
function stopSpinner(message) {
|
|
1744
|
+
if (spinnerTimer) {
|
|
1745
|
+
clearInterval(spinnerTimer);
|
|
1746
|
+
spinnerTimer = null;
|
|
1747
|
+
}
|
|
1748
|
+
if (!isTTY()) return;
|
|
1749
|
+
if (message) {
|
|
1750
|
+
writeOSC(message);
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
function notify() {
|
|
1754
|
+
if (!isTTY()) return;
|
|
1755
|
+
process.stdout.write("\x07");
|
|
1756
|
+
}
|
|
1757
|
+
function resetTitle() {
|
|
1758
|
+
if (!isTTY()) return;
|
|
1759
|
+
writeOSC("");
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1718
1762
|
// src/worktree.ts
|
|
1719
1763
|
import { appendFileSync as appendFileSync5, existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
|
|
1720
1764
|
import { join as join6, resolve as resolve4 } from "path";
|
|
@@ -1946,6 +1990,8 @@ function installSignalHandlers() {
|
|
|
1946
1990
|
process.exit(1);
|
|
1947
1991
|
}
|
|
1948
1992
|
shuttingDown = true;
|
|
1993
|
+
stopSpinner();
|
|
1994
|
+
resetTitle();
|
|
1949
1995
|
warn(`Received ${signal}. Reverting active issue...`);
|
|
1950
1996
|
if (activeCleanup) {
|
|
1951
1997
|
const { issueId, previousStatus, source } = activeCleanup;
|
|
@@ -2022,12 +2068,14 @@ async function runLoop(config2, opts) {
|
|
|
2022
2068
|
const timestamp2 = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").substring(0, 19);
|
|
2023
2069
|
const logFile = resolve5(config2.logs.dir, `session_${session}_${timestamp2}.log`);
|
|
2024
2070
|
divider(session);
|
|
2071
|
+
startSpinner("fetching issue...");
|
|
2025
2072
|
if (opts.issueId) {
|
|
2026
2073
|
log(`Fetching issue '${opts.issueId}' from ${config2.source}...`);
|
|
2027
2074
|
} else {
|
|
2028
2075
|
log(`Fetching next '${config2.source_config.label}' issue from ${config2.source}...`);
|
|
2029
2076
|
}
|
|
2030
2077
|
if (opts.dryRun) {
|
|
2078
|
+
stopSpinner();
|
|
2031
2079
|
if (opts.issueId) {
|
|
2032
2080
|
log(`[dry-run] Would fetch issue '${opts.issueId}' from ${config2.source}`);
|
|
2033
2081
|
} else {
|
|
@@ -2044,11 +2092,14 @@ async function runLoop(config2, opts) {
|
|
|
2044
2092
|
try {
|
|
2045
2093
|
issue = opts.issueId ? await source.fetchIssueById(opts.issueId) : await source.fetchNextIssue(config2.source_config);
|
|
2046
2094
|
} catch (err) {
|
|
2095
|
+
stopSpinner();
|
|
2047
2096
|
error(`Failed to fetch issues: ${err instanceof Error ? err.message : String(err)}`);
|
|
2048
2097
|
if (opts.once) break;
|
|
2098
|
+
setTitle("Lisa \u2014 cooling down...");
|
|
2049
2099
|
await sleep(config2.loop.cooldown * 1e3);
|
|
2050
2100
|
continue;
|
|
2051
2101
|
}
|
|
2102
|
+
stopSpinner();
|
|
2052
2103
|
if (!issue) {
|
|
2053
2104
|
if (opts.issueId) {
|
|
2054
2105
|
error(`Issue '${opts.issueId}' not found.`);
|
|
@@ -2058,6 +2109,7 @@ async function runLoop(config2, opts) {
|
|
|
2058
2109
|
break;
|
|
2059
2110
|
}
|
|
2060
2111
|
ok(`Picked up: ${issue.id} \u2014 ${issue.title}`);
|
|
2112
|
+
setTitle(`Lisa \u2014 ${issue.id}`);
|
|
2061
2113
|
const previousStatus = config2.source_config.pick_from;
|
|
2062
2114
|
try {
|
|
2063
2115
|
const inProgress = config2.source_config.in_progress;
|
|
@@ -2071,6 +2123,7 @@ async function runLoop(config2, opts) {
|
|
|
2071
2123
|
try {
|
|
2072
2124
|
sessionResult = config2.workflow === "worktree" ? await runWorktreeSession(config2, issue, logFile, session, models) : await runBranchSession(config2, issue, logFile, session, models);
|
|
2073
2125
|
} catch (err) {
|
|
2126
|
+
stopSpinner();
|
|
2074
2127
|
error(
|
|
2075
2128
|
`Unhandled error in session for ${issue.id}: ${err instanceof Error ? err.message : String(err)}`
|
|
2076
2129
|
);
|
|
@@ -2083,8 +2136,10 @@ async function runLoop(config2, opts) {
|
|
|
2083
2136
|
);
|
|
2084
2137
|
}
|
|
2085
2138
|
activeCleanup = null;
|
|
2139
|
+
notify();
|
|
2086
2140
|
if (opts.once) break;
|
|
2087
2141
|
log(`Cooling down ${config2.loop.cooldown}s before next issue...`);
|
|
2142
|
+
setTitle("Lisa \u2014 cooling down...");
|
|
2088
2143
|
await sleep(config2.loop.cooldown * 1e3);
|
|
2089
2144
|
continue;
|
|
2090
2145
|
}
|
|
@@ -2100,11 +2155,13 @@ async function runLoop(config2, opts) {
|
|
|
2100
2155
|
);
|
|
2101
2156
|
}
|
|
2102
2157
|
activeCleanup = null;
|
|
2158
|
+
notify();
|
|
2103
2159
|
if (opts.once) {
|
|
2104
2160
|
log("Single iteration mode. Exiting.");
|
|
2105
2161
|
break;
|
|
2106
2162
|
}
|
|
2107
2163
|
log(`Cooling down ${config2.loop.cooldown}s before next issue...`);
|
|
2164
|
+
setTitle("Lisa \u2014 cooling down...");
|
|
2108
2165
|
await sleep(config2.loop.cooldown * 1e3);
|
|
2109
2166
|
continue;
|
|
2110
2167
|
}
|
|
@@ -2122,11 +2179,13 @@ async function runLoop(config2, opts) {
|
|
|
2122
2179
|
);
|
|
2123
2180
|
}
|
|
2124
2181
|
activeCleanup = null;
|
|
2182
|
+
notify();
|
|
2125
2183
|
if (opts.once) {
|
|
2126
2184
|
log("Single iteration mode. Exiting.");
|
|
2127
2185
|
break;
|
|
2128
2186
|
}
|
|
2129
2187
|
log(`Cooling down ${config2.loop.cooldown}s before next issue...`);
|
|
2188
|
+
setTitle("Lisa \u2014 cooling down...");
|
|
2130
2189
|
await sleep(config2.loop.cooldown * 1e3);
|
|
2131
2190
|
continue;
|
|
2132
2191
|
}
|
|
@@ -2150,13 +2209,17 @@ async function runLoop(config2, opts) {
|
|
|
2150
2209
|
error(`Failed to complete issue: ${err instanceof Error ? err.message : String(err)}`);
|
|
2151
2210
|
}
|
|
2152
2211
|
activeCleanup = null;
|
|
2212
|
+
stopSpinner(`\u2713 Lisa \u2014 ${issue.id} \u2014 PR created`);
|
|
2213
|
+
notify();
|
|
2153
2214
|
if (opts.once) {
|
|
2154
2215
|
log("Single iteration mode. Exiting.");
|
|
2155
2216
|
break;
|
|
2156
2217
|
}
|
|
2157
2218
|
log(`Cooling down ${config2.loop.cooldown}s before next issue...`);
|
|
2219
|
+
setTitle("Lisa \u2014 cooling down...");
|
|
2158
2220
|
await sleep(config2.loop.cooldown * 1e3);
|
|
2159
2221
|
}
|
|
2222
|
+
resetTitle();
|
|
2160
2223
|
ok(`lisa finished. ${session} session(s) run.`);
|
|
2161
2224
|
}
|
|
2162
2225
|
function logAttemptHistory(result) {
|
|
@@ -2205,11 +2268,13 @@ async function runWorktreeSession(config2, issue, logFile, session, models) {
|
|
|
2205
2268
|
const repoPath = determineRepoPath(config2.repos, issue, workspace) ?? workspace;
|
|
2206
2269
|
const defaultBranch = resolveBaseBranch(config2, repoPath);
|
|
2207
2270
|
const branchName = generateBranchName(issue.id, issue.title);
|
|
2271
|
+
startSpinner(`${issue.id} \u2014 creating worktree...`);
|
|
2208
2272
|
log(`Creating worktree for ${branchName} (base: ${defaultBranch})...`);
|
|
2209
2273
|
let worktreePath;
|
|
2210
2274
|
try {
|
|
2211
2275
|
worktreePath = await createWorktree(repoPath, branchName, defaultBranch);
|
|
2212
2276
|
} catch (err) {
|
|
2277
|
+
stopSpinner();
|
|
2213
2278
|
error(`Failed to create worktree: ${err instanceof Error ? err.message : String(err)}`);
|
|
2214
2279
|
return {
|
|
2215
2280
|
success: false,
|
|
@@ -2224,10 +2289,13 @@ async function runWorktreeSession(config2, issue, logFile, session, models) {
|
|
|
2224
2289
|
}
|
|
2225
2290
|
};
|
|
2226
2291
|
}
|
|
2292
|
+
stopSpinner();
|
|
2227
2293
|
ok(`Worktree created at ${worktreePath}`);
|
|
2228
2294
|
const repo = findRepoConfig(config2, issue);
|
|
2229
2295
|
if (repo?.lifecycle) {
|
|
2296
|
+
startSpinner(`${issue.id} \u2014 starting resources...`);
|
|
2230
2297
|
const started = await startResources(repo, worktreePath);
|
|
2298
|
+
stopSpinner();
|
|
2231
2299
|
if (!started) {
|
|
2232
2300
|
error(`Lifecycle startup failed for ${issue.id}. Aborting session.`);
|
|
2233
2301
|
await cleanupWorktree(repoPath, worktreePath);
|
|
@@ -2250,6 +2318,7 @@ async function runWorktreeSession(config2, issue, logFile, session, models) {
|
|
|
2250
2318
|
log(`Detected test runner: ${testRunner}`);
|
|
2251
2319
|
}
|
|
2252
2320
|
const prompt = buildImplementPrompt(issue, config2, testRunner);
|
|
2321
|
+
startSpinner(`${issue.id} \u2014 implementing...`);
|
|
2253
2322
|
log(`Implementing in worktree... (log: ${logFile})`);
|
|
2254
2323
|
initLogFile(logFile);
|
|
2255
2324
|
const result = await runWithFallback(models, prompt, {
|
|
@@ -2259,6 +2328,7 @@ async function runWorktreeSession(config2, issue, logFile, session, models) {
|
|
|
2259
2328
|
issueId: issue.id,
|
|
2260
2329
|
overseer: config2.overseer
|
|
2261
2330
|
});
|
|
2331
|
+
stopSpinner();
|
|
2262
2332
|
try {
|
|
2263
2333
|
appendFileSync6(
|
|
2264
2334
|
logFile,
|
|
@@ -2279,7 +2349,9 @@ ${result.output}
|
|
|
2279
2349
|
await cleanupWorktree(repoPath, worktreePath);
|
|
2280
2350
|
return { success: false, providerUsed: result.providerUsed, prUrls: [], fallback: result };
|
|
2281
2351
|
}
|
|
2352
|
+
startSpinner(`${issue.id} \u2014 validating tests...`);
|
|
2282
2353
|
const testsPassed = await runTestValidation(worktreePath);
|
|
2354
|
+
stopSpinner();
|
|
2283
2355
|
if (!testsPassed) {
|
|
2284
2356
|
error(`Tests failed for ${issue.id}. Blocking PR creation.`);
|
|
2285
2357
|
await cleanupWorktree(repoPath, worktreePath);
|
|
@@ -2299,6 +2371,7 @@ ${result.output}
|
|
|
2299
2371
|
);
|
|
2300
2372
|
}
|
|
2301
2373
|
}
|
|
2374
|
+
startSpinner(`${issue.id} \u2014 pushing...`);
|
|
2302
2375
|
const pushResult = await pushWithRecovery({
|
|
2303
2376
|
branch: effectiveBranch,
|
|
2304
2377
|
cwd: worktreePath,
|
|
@@ -2308,12 +2381,14 @@ ${result.output}
|
|
|
2308
2381
|
issueId: issue.id,
|
|
2309
2382
|
overseer: config2.overseer
|
|
2310
2383
|
});
|
|
2384
|
+
stopSpinner();
|
|
2311
2385
|
if (!pushResult.success) {
|
|
2312
2386
|
error(`Failed to push branch to remote: ${pushResult.error}`);
|
|
2313
2387
|
cleanupManifest(worktreePath);
|
|
2314
2388
|
await cleanupWorktree(repoPath, worktreePath);
|
|
2315
2389
|
return { success: false, providerUsed: result.providerUsed, prUrls: [], fallback: result };
|
|
2316
2390
|
}
|
|
2391
|
+
startSpinner(`${issue.id} \u2014 creating PR...`);
|
|
2317
2392
|
const prTitle = manifest?.prTitle ?? readPrTitle(worktreePath) ?? issue.title;
|
|
2318
2393
|
cleanupPrTitle(worktreePath);
|
|
2319
2394
|
cleanupManifest(worktreePath);
|
|
@@ -2336,6 +2411,7 @@ ${result.output}
|
|
|
2336
2411
|
} catch (err) {
|
|
2337
2412
|
error(`Failed to create PR: ${err instanceof Error ? err.message : String(err)}`);
|
|
2338
2413
|
}
|
|
2414
|
+
stopSpinner();
|
|
2339
2415
|
await cleanupWorktree(repoPath, worktreePath);
|
|
2340
2416
|
ok(`Session ${session} complete for ${issue.id}`);
|
|
2341
2417
|
return { success: true, providerUsed: result.providerUsed, prUrls, fallback: result };
|
|
@@ -2344,6 +2420,7 @@ async function runWorktreeMultiRepoSession(config2, issue, logFile, session, mod
|
|
|
2344
2420
|
const workspace = resolve5(config2.workspace);
|
|
2345
2421
|
cleanupManifest(workspace);
|
|
2346
2422
|
const prompt = buildWorktreeMultiRepoPrompt(issue, config2);
|
|
2423
|
+
startSpinner(`${issue.id} \u2014 implementing...`);
|
|
2347
2424
|
log(`Multi-repo worktree session for ${issue.id} (agent selects repo and branch name)`);
|
|
2348
2425
|
log(`Implementing (agent selects repo)... (log: ${logFile})`);
|
|
2349
2426
|
initLogFile(logFile);
|
|
@@ -2354,6 +2431,7 @@ async function runWorktreeMultiRepoSession(config2, issue, logFile, session, mod
|
|
|
2354
2431
|
issueId: issue.id,
|
|
2355
2432
|
overseer: config2.overseer
|
|
2356
2433
|
});
|
|
2434
|
+
stopSpinner();
|
|
2357
2435
|
try {
|
|
2358
2436
|
appendFileSync6(
|
|
2359
2437
|
logFile,
|
|
@@ -2387,13 +2465,16 @@ ${result.output}
|
|
|
2387
2465
|
if (!hasWorktree) {
|
|
2388
2466
|
warn(`Worktree not found at ${worktreePath} \u2014 using repo root for git operations`);
|
|
2389
2467
|
}
|
|
2468
|
+
startSpinner(`${issue.id} \u2014 validating tests...`);
|
|
2390
2469
|
const testsPassed = await runTestValidation(effectiveCwd);
|
|
2470
|
+
stopSpinner();
|
|
2391
2471
|
if (!testsPassed) {
|
|
2392
2472
|
error(`Tests failed for ${issue.id}. Blocking PR creation.`);
|
|
2393
2473
|
if (hasWorktree) await cleanupWorktree(manifest.repoPath, worktreePath);
|
|
2394
2474
|
cleanupManifest(workspace);
|
|
2395
2475
|
return { success: false, providerUsed: result.providerUsed, prUrls: [], fallback: result };
|
|
2396
2476
|
}
|
|
2477
|
+
startSpinner(`${issue.id} \u2014 pushing...`);
|
|
2397
2478
|
const pushResult = await pushWithRecovery({
|
|
2398
2479
|
branch: manifest.branch,
|
|
2399
2480
|
cwd: effectiveCwd,
|
|
@@ -2403,12 +2484,14 @@ ${result.output}
|
|
|
2403
2484
|
issueId: issue.id,
|
|
2404
2485
|
overseer: config2.overseer
|
|
2405
2486
|
});
|
|
2487
|
+
stopSpinner();
|
|
2406
2488
|
if (!pushResult.success) {
|
|
2407
2489
|
error(`Failed to push branch to remote: ${pushResult.error}`);
|
|
2408
2490
|
if (hasWorktree) await cleanupWorktree(manifest.repoPath, worktreePath);
|
|
2409
2491
|
cleanupManifest(workspace);
|
|
2410
2492
|
return { success: false, providerUsed: result.providerUsed, prUrls: [], fallback: result };
|
|
2411
2493
|
}
|
|
2494
|
+
startSpinner(`${issue.id} \u2014 creating PR...`);
|
|
2412
2495
|
const prTitle = manifest.prTitle ?? issue.title;
|
|
2413
2496
|
const prUrls = [];
|
|
2414
2497
|
try {
|
|
@@ -2429,6 +2512,7 @@ ${result.output}
|
|
|
2429
2512
|
} catch (err) {
|
|
2430
2513
|
error(`Failed to create PR: ${err instanceof Error ? err.message : String(err)}`);
|
|
2431
2514
|
}
|
|
2515
|
+
stopSpinner();
|
|
2432
2516
|
cleanupManifest(workspace);
|
|
2433
2517
|
if (hasWorktree) await cleanupWorktree(manifest.repoPath, worktreePath);
|
|
2434
2518
|
ok(`Session ${session} complete for ${issue.id}`);
|
|
@@ -2444,8 +2528,10 @@ async function runBranchSession(config2, issue, logFile, session, models) {
|
|
|
2444
2528
|
const prompt = buildImplementPrompt(issue, config2, testRunner);
|
|
2445
2529
|
const repo = findRepoConfig(config2, issue);
|
|
2446
2530
|
if (repo?.lifecycle) {
|
|
2531
|
+
startSpinner(`${issue.id} \u2014 starting resources...`);
|
|
2447
2532
|
const cwd = resolve5(workspace, repo.path);
|
|
2448
2533
|
const started = await startResources(repo, cwd);
|
|
2534
|
+
stopSpinner();
|
|
2449
2535
|
if (!started) {
|
|
2450
2536
|
error(`Lifecycle startup failed for ${issue.id}. Aborting session.`);
|
|
2451
2537
|
return {
|
|
@@ -2462,6 +2548,7 @@ async function runBranchSession(config2, issue, logFile, session, models) {
|
|
|
2462
2548
|
};
|
|
2463
2549
|
}
|
|
2464
2550
|
}
|
|
2551
|
+
startSpinner(`${issue.id} \u2014 implementing...`);
|
|
2465
2552
|
log(`Implementing... (log: ${logFile})`);
|
|
2466
2553
|
initLogFile(logFile);
|
|
2467
2554
|
const result = await runWithFallback(models, prompt, {
|
|
@@ -2471,6 +2558,7 @@ async function runBranchSession(config2, issue, logFile, session, models) {
|
|
|
2471
2558
|
issueId: issue.id,
|
|
2472
2559
|
overseer: config2.overseer
|
|
2473
2560
|
});
|
|
2561
|
+
stopSpinner();
|
|
2474
2562
|
try {
|
|
2475
2563
|
appendFileSync6(
|
|
2476
2564
|
logFile,
|
|
@@ -2490,7 +2578,9 @@ ${result.output}
|
|
|
2490
2578
|
error(`Session ${session} failed for ${issue.id}. Check ${logFile}`);
|
|
2491
2579
|
return { success: false, providerUsed: result.providerUsed, prUrls: [], fallback: result };
|
|
2492
2580
|
}
|
|
2581
|
+
startSpinner(`${issue.id} \u2014 validating tests...`);
|
|
2493
2582
|
const testsPassed = await runTestValidation(workspace);
|
|
2583
|
+
stopSpinner();
|
|
2494
2584
|
if (!testsPassed) {
|
|
2495
2585
|
error(`Tests failed for ${issue.id}. Blocking PR creation.`);
|
|
2496
2586
|
cleanupManifest(workspace);
|
|
@@ -2513,6 +2603,7 @@ ${result.output}
|
|
|
2513
2603
|
ok(`Session ${session} complete for ${issue.id}`);
|
|
2514
2604
|
return { success: true, providerUsed: result.providerUsed, prUrls: [], fallback: result };
|
|
2515
2605
|
}
|
|
2606
|
+
startSpinner(`${issue.id} \u2014 creating PR...`);
|
|
2516
2607
|
const prTitle = manifest?.prTitle ?? readPrTitle(workspace) ?? issue.title;
|
|
2517
2608
|
cleanupPrTitle(workspace);
|
|
2518
2609
|
const prUrls = [];
|
|
@@ -2538,6 +2629,7 @@ ${result.output}
|
|
|
2538
2629
|
error(`Failed to create PR: ${err instanceof Error ? err.message : String(err)}`);
|
|
2539
2630
|
}
|
|
2540
2631
|
}
|
|
2632
|
+
stopSpinner();
|
|
2541
2633
|
ok(`Session ${session} complete for ${issue.id}`);
|
|
2542
2634
|
return { success: true, providerUsed: result.providerUsed, prUrls, fallback: result };
|
|
2543
2635
|
}
|