@hasna/todos 0.13.12 → 0.15.1
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 +2 -0
- package/dist/cli/commands/agent-commands.d.ts +39 -0
- package/dist/cli/commands/agent-commands.d.ts.map +1 -1
- package/dist/cli/commands/config-serve-commands.d.ts.map +1 -1
- package/dist/cli/commands/dispatch.d.ts.map +1 -1
- package/dist/cli/commands/mcp-hooks-commands.d.ts.map +1 -1
- package/dist/cli/commands/project-commands.d.ts.map +1 -1
- package/dist/cli/commands/task-commands.d.ts.map +1 -1
- package/dist/cli/helpers.d.ts +64 -2
- package/dist/cli/helpers.d.ts.map +1 -1
- package/dist/cli/index.js +624 -98
- package/dist/cli/stage-a.d.ts.map +1 -1
- package/dist/contracts.js +123 -116
- package/dist/db/database.d.ts.map +1 -1
- package/dist/db/task-crud.d.ts.map +1 -1
- package/dist/index.js +135 -127
- package/dist/lib/bulk-tags.d.ts +57 -0
- package/dist/lib/bulk-tags.d.ts.map +1 -0
- package/dist/lib/enum-vocabulary.d.ts +72 -0
- package/dist/lib/enum-vocabulary.d.ts.map +1 -0
- package/dist/lib/run-records.d.ts.map +1 -1
- package/dist/lib/sandbox-profiles.d.ts.map +1 -1
- package/dist/lib/saved-search-views.d.ts +12 -1
- package/dist/lib/saved-search-views.d.ts.map +1 -1
- package/dist/lib/sync-utils.d.ts +1 -1
- package/dist/lib/sync-utils.d.ts.map +1 -1
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +289 -82
- package/dist/mcp/tools/agents.d.ts.map +1 -1
- package/dist/mcp/tools/task-auto-tools.d.ts.map +1 -1
- package/dist/mcp/tools/task-crud.d.ts +1 -0
- package/dist/mcp/tools/task-crud.d.ts.map +1 -1
- package/dist/mcp.js +1 -1
- package/dist/registry.js +123 -116
- package/dist/release-provenance.json +5 -5
- package/dist/sdk/index.js +3 -2
- package/dist/sdk/v1.generated.d.ts +2 -2
- package/dist/sdk/v1.generated.d.ts.map +1 -1
- package/dist/server/index.js +245 -38
- package/dist/server/openapi.d.ts +58 -0
- package/dist/server/openapi.d.ts.map +1 -1
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/v1.d.ts.map +1 -1
- package/dist/storage.js +124 -115
- package/dist/types/index.d.ts +11 -0
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/mcp/index.js
CHANGED
|
@@ -44,7 +44,10 @@ var __require = import.meta.require;
|
|
|
44
44
|
function isBlockingDependencyStatus(status) {
|
|
45
45
|
return status !== "completed" && status !== "cancelled";
|
|
46
46
|
}
|
|
47
|
-
|
|
47
|
+
function isTerminalStatus(status) {
|
|
48
|
+
return status === "completed" || status === "failed" || status === "cancelled";
|
|
49
|
+
}
|
|
50
|
+
var TASK_STATUSES, TASK_PRIORITIES, VersionConflictError, TaskNotFoundError, TaskNotStartableError, TaskReferenceAmbiguousError, ProjectNotFoundError, ResourceConflictError, PlanNotFoundError, LockError, AgentNotFoundError, IdentityAliasAmbiguousError, IdentityIdImmutableError, TaskListNotFoundError, DependencyCycleError, CompletionGuardError, DispatchNotFoundError;
|
|
48
51
|
var init_types = __esm(() => {
|
|
49
52
|
TASK_STATUSES = [
|
|
50
53
|
"pending",
|
|
@@ -53,6 +56,12 @@ var init_types = __esm(() => {
|
|
|
53
56
|
"failed",
|
|
54
57
|
"cancelled"
|
|
55
58
|
];
|
|
59
|
+
TASK_PRIORITIES = [
|
|
60
|
+
"low",
|
|
61
|
+
"medium",
|
|
62
|
+
"high",
|
|
63
|
+
"critical"
|
|
64
|
+
];
|
|
56
65
|
VersionConflictError = class VersionConflictError extends Error {
|
|
57
66
|
taskId;
|
|
58
67
|
expectedVersion;
|
|
@@ -3795,6 +3804,41 @@ var init_identity_mapping = __esm(() => {
|
|
|
3795
3804
|
});
|
|
3796
3805
|
});
|
|
3797
3806
|
|
|
3807
|
+
// src/lib/sync-utils.ts
|
|
3808
|
+
import { existsSync as existsSync2, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
|
|
3809
|
+
import { homedir } from "os";
|
|
3810
|
+
import { join } from "path";
|
|
3811
|
+
function getHomeDir() {
|
|
3812
|
+
return process.env["HOME"] || process.env["USERPROFILE"] || homedir();
|
|
3813
|
+
}
|
|
3814
|
+
function getTodosGlobalDir() {
|
|
3815
|
+
return join(getHomeDir(), ".hasna", "todos");
|
|
3816
|
+
}
|
|
3817
|
+
function ensureDir(dir) {
|
|
3818
|
+
if (!existsSync2(dir))
|
|
3819
|
+
mkdirSync(dir, { recursive: true });
|
|
3820
|
+
}
|
|
3821
|
+
function readJsonFile(path) {
|
|
3822
|
+
try {
|
|
3823
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
3824
|
+
} catch {
|
|
3825
|
+
return null;
|
|
3826
|
+
}
|
|
3827
|
+
}
|
|
3828
|
+
function writeJsonFile(path, data) {
|
|
3829
|
+
writeFileSync(path, JSON.stringify(data, null, 2) + `
|
|
3830
|
+
`);
|
|
3831
|
+
}
|
|
3832
|
+
function appendSyncConflict(metadata, conflict, limit = 5) {
|
|
3833
|
+
const current = Array.isArray(metadata["sync_conflicts"]) ? metadata["sync_conflicts"] : [];
|
|
3834
|
+
const next = [conflict, ...current].slice(0, limit);
|
|
3835
|
+
return { ...metadata, sync_conflicts: next };
|
|
3836
|
+
}
|
|
3837
|
+
var HOME;
|
|
3838
|
+
var init_sync_utils = __esm(() => {
|
|
3839
|
+
HOME = getHomeDir();
|
|
3840
|
+
});
|
|
3841
|
+
|
|
3798
3842
|
// src/storage/config.ts
|
|
3799
3843
|
var exports_config = {};
|
|
3800
3844
|
__export(exports_config, {
|
|
@@ -4125,8 +4169,8 @@ __export(exports_database, {
|
|
|
4125
4169
|
LOCK_EXPIRY_MINUTES: () => LOCK_EXPIRY_MINUTES
|
|
4126
4170
|
});
|
|
4127
4171
|
import { Database } from "bun:sqlite";
|
|
4128
|
-
import { existsSync as
|
|
4129
|
-
import { dirname, join, resolve as resolve2 } from "path";
|
|
4172
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2 } from "fs";
|
|
4173
|
+
import { dirname, join as join2, resolve as resolve2 } from "path";
|
|
4130
4174
|
function isInMemoryDb(path) {
|
|
4131
4175
|
return path === ":memory:" || path.startsWith("file::memory:");
|
|
4132
4176
|
}
|
|
@@ -4135,8 +4179,8 @@ function findNearestProjectDb(startDir) {
|
|
|
4135
4179
|
const stopAt = gitRoot ? resolve2(gitRoot) : resolve2(startDir);
|
|
4136
4180
|
let dir = resolve2(startDir);
|
|
4137
4181
|
while (true) {
|
|
4138
|
-
const candidate =
|
|
4139
|
-
if (
|
|
4182
|
+
const candidate = join2(dir, ".hasna", "todos", "todos.db");
|
|
4183
|
+
if (existsSync3(candidate))
|
|
4140
4184
|
return candidate;
|
|
4141
4185
|
if (dir === stopAt)
|
|
4142
4186
|
break;
|
|
@@ -4150,7 +4194,7 @@ function findNearestProjectDb(startDir) {
|
|
|
4150
4194
|
function findGitRoot(startDir) {
|
|
4151
4195
|
let dir = resolve2(startDir);
|
|
4152
4196
|
while (true) {
|
|
4153
|
-
if (
|
|
4197
|
+
if (existsSync3(join2(dir, ".git")))
|
|
4154
4198
|
return dir;
|
|
4155
4199
|
const parent = dirname(dir);
|
|
4156
4200
|
if (parent === dir)
|
|
@@ -4160,8 +4204,7 @@ function findGitRoot(startDir) {
|
|
|
4160
4204
|
return null;
|
|
4161
4205
|
}
|
|
4162
4206
|
function getGlobalDbPath() {
|
|
4163
|
-
|
|
4164
|
-
return join(home, ".hasna", "todos", "todos.db");
|
|
4207
|
+
return join2(getHomeDir(), ".hasna", "todos", "todos.db");
|
|
4165
4208
|
}
|
|
4166
4209
|
function hasExplicitProjectArg(args = process.argv.slice(2)) {
|
|
4167
4210
|
return args.some((arg) => arg === "--project" || arg.startsWith("--project="));
|
|
@@ -4199,7 +4242,7 @@ function getDbPath() {
|
|
|
4199
4242
|
if (process.env["TODOS_DB_SCOPE"] === "project") {
|
|
4200
4243
|
const gitRoot = findGitRoot(cwd);
|
|
4201
4244
|
if (gitRoot && canCreateScopedProjectDb()) {
|
|
4202
|
-
return
|
|
4245
|
+
return join2(gitRoot, ".hasna", "todos", "todos.db");
|
|
4203
4246
|
}
|
|
4204
4247
|
}
|
|
4205
4248
|
return getGlobalDbPath();
|
|
@@ -4207,16 +4250,16 @@ function getDbPath() {
|
|
|
4207
4250
|
function getDatabasePath() {
|
|
4208
4251
|
return getDbPath();
|
|
4209
4252
|
}
|
|
4210
|
-
function
|
|
4253
|
+
function ensureDir2(filePath) {
|
|
4211
4254
|
if (isInMemoryDb(filePath))
|
|
4212
4255
|
return;
|
|
4213
4256
|
const dir = dirname(resolve2(filePath));
|
|
4214
|
-
if (!
|
|
4215
|
-
|
|
4257
|
+
if (!existsSync3(dir)) {
|
|
4258
|
+
mkdirSync2(dir, { recursive: true });
|
|
4216
4259
|
}
|
|
4217
4260
|
}
|
|
4218
4261
|
function openDatabase(path) {
|
|
4219
|
-
|
|
4262
|
+
ensureDir2(path);
|
|
4220
4263
|
const db = new Database(path);
|
|
4221
4264
|
db.run("PRAGMA busy_timeout = 5000");
|
|
4222
4265
|
db.run("PRAGMA journal_mode = WAL");
|
|
@@ -4383,6 +4426,7 @@ var init_database = __esm(() => {
|
|
|
4383
4426
|
init_machines();
|
|
4384
4427
|
init_identity_mapping();
|
|
4385
4428
|
init_types();
|
|
4429
|
+
init_sync_utils();
|
|
4386
4430
|
ALLOWED_TABLES = new Set(["tasks", "projects", "agents", "plans", "task_lists", "task_templates", "project_knowledge_records", "project_risks", "local_retrospectives"]);
|
|
4387
4431
|
});
|
|
4388
4432
|
|
|
@@ -9126,40 +9170,6 @@ var init_zod = __esm(() => {
|
|
|
9126
9170
|
init_external();
|
|
9127
9171
|
});
|
|
9128
9172
|
|
|
9129
|
-
// src/lib/sync-utils.ts
|
|
9130
|
-
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync, readdirSync, statSync, writeFileSync } from "fs";
|
|
9131
|
-
import { join as join2 } from "path";
|
|
9132
|
-
function getHomeDir() {
|
|
9133
|
-
return process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
9134
|
-
}
|
|
9135
|
-
function getTodosGlobalDir() {
|
|
9136
|
-
return join2(getHomeDir(), ".hasna", "todos");
|
|
9137
|
-
}
|
|
9138
|
-
function ensureDir2(dir) {
|
|
9139
|
-
if (!existsSync3(dir))
|
|
9140
|
-
mkdirSync2(dir, { recursive: true });
|
|
9141
|
-
}
|
|
9142
|
-
function readJsonFile(path) {
|
|
9143
|
-
try {
|
|
9144
|
-
return JSON.parse(readFileSync(path, "utf-8"));
|
|
9145
|
-
} catch {
|
|
9146
|
-
return null;
|
|
9147
|
-
}
|
|
9148
|
-
}
|
|
9149
|
-
function writeJsonFile(path, data) {
|
|
9150
|
-
writeFileSync(path, JSON.stringify(data, null, 2) + `
|
|
9151
|
-
`);
|
|
9152
|
-
}
|
|
9153
|
-
function appendSyncConflict(metadata, conflict, limit = 5) {
|
|
9154
|
-
const current = Array.isArray(metadata["sync_conflicts"]) ? metadata["sync_conflicts"] : [];
|
|
9155
|
-
const next = [conflict, ...current].slice(0, limit);
|
|
9156
|
-
return { ...metadata, sync_conflicts: next };
|
|
9157
|
-
}
|
|
9158
|
-
var HOME;
|
|
9159
|
-
var init_sync_utils = __esm(() => {
|
|
9160
|
-
HOME = process.env["HOME"] || process.env["USERPROFILE"] || "~";
|
|
9161
|
-
});
|
|
9162
|
-
|
|
9163
9173
|
// src/lib/config.ts
|
|
9164
9174
|
import { existsSync as existsSync4 } from "fs";
|
|
9165
9175
|
import { dirname as dirname2, join as join3 } from "path";
|
|
@@ -9182,7 +9192,7 @@ function loadConfig() {
|
|
|
9182
9192
|
}
|
|
9183
9193
|
function saveConfig(config) {
|
|
9184
9194
|
const configPath = getConfigPath();
|
|
9185
|
-
|
|
9195
|
+
ensureDir(dirname2(configPath));
|
|
9186
9196
|
writeJsonFile(configPath, config);
|
|
9187
9197
|
cached = config;
|
|
9188
9198
|
return config;
|
|
@@ -10737,7 +10747,7 @@ var init_event_hooks = __esm(() => {
|
|
|
10737
10747
|
// node_modules/.bun/@hasna+events@0.1.11/node_modules/@hasna/events/dist/index.js
|
|
10738
10748
|
import { chmod, mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
10739
10749
|
import { existsSync as existsSync5 } from "fs";
|
|
10740
|
-
import { homedir } from "os";
|
|
10750
|
+
import { homedir as homedir2 } from "os";
|
|
10741
10751
|
import { join as join4 } from "path";
|
|
10742
10752
|
import { createHmac, timingSafeEqual } from "crypto";
|
|
10743
10753
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
@@ -10839,7 +10849,7 @@ function channelMatchesEvent(channel, event) {
|
|
|
10839
10849
|
return channel.filters.some((filter) => eventMatchesFilter(event, filter));
|
|
10840
10850
|
}
|
|
10841
10851
|
function getEventsDataDir(override) {
|
|
10842
|
-
return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join4(
|
|
10852
|
+
return override || process.env[HASNA_EVENTS_DIR_ENV] || process.env[HASNA_EVENTS_HOME_ENV] || join4(homedir2(), ".hasna", "events");
|
|
10843
10853
|
}
|
|
10844
10854
|
|
|
10845
10855
|
class JsonEventsStore {
|
|
@@ -13927,11 +13937,13 @@ function updateTask(id, input, db) {
|
|
|
13927
13937
|
}
|
|
13928
13938
|
sets.push("status = ?");
|
|
13929
13939
|
params.push(input.status);
|
|
13940
|
+
if (isTerminalStatus(input.status)) {
|
|
13941
|
+
sets.push("locked_by = NULL");
|
|
13942
|
+
sets.push("locked_at = NULL");
|
|
13943
|
+
}
|
|
13930
13944
|
if (input.status === "completed") {
|
|
13931
13945
|
sets.push("completed_at = ?");
|
|
13932
13946
|
params.push(completionTimestamp);
|
|
13933
|
-
sets.push("locked_by = NULL");
|
|
13934
|
-
sets.push("locked_at = NULL");
|
|
13935
13947
|
} else if (task.status === "completed" && input.completed_at === undefined) {
|
|
13936
13948
|
sets.push("completed_at = NULL");
|
|
13937
13949
|
}
|
|
@@ -14055,6 +14067,7 @@ function updateTask(id, input, db) {
|
|
|
14055
14067
|
logTaskChange(id, "approve", "approved_by", null, input.approved_by, agentId, d);
|
|
14056
14068
|
const reopened = input.status !== undefined && input.status !== "completed" && task.status === "completed" && input.completed_at === undefined;
|
|
14057
14069
|
const completedNow = input.status === "completed";
|
|
14070
|
+
const terminalNow = input.status !== undefined && isTerminalStatus(input.status);
|
|
14058
14071
|
const updatedTask = {
|
|
14059
14072
|
...task,
|
|
14060
14073
|
...Object.fromEntries(Object.entries(input).filter(([, v]) => v !== undefined)),
|
|
@@ -14062,8 +14075,8 @@ function updateTask(id, input, db) {
|
|
|
14062
14075
|
metadata: input.metadata ?? task.metadata,
|
|
14063
14076
|
version: task.version + 1,
|
|
14064
14077
|
updated_at: timestamp2,
|
|
14065
|
-
locked_by:
|
|
14066
|
-
locked_at:
|
|
14078
|
+
locked_by: terminalNow ? null : task.locked_by,
|
|
14079
|
+
locked_at: terminalNow ? null : task.locked_at,
|
|
14067
14080
|
completed_at: completedNow ? completionTimestamp : reopened ? null : input.completed_at !== undefined ? input.completed_at : task.completed_at,
|
|
14068
14081
|
sla_minutes: input.sla_minutes !== undefined ? input.sla_minutes : task.sla_minutes,
|
|
14069
14082
|
actual_minutes: input.actual_minutes ?? task.actual_minutes,
|
|
@@ -17990,10 +18003,10 @@ var init_token_utils = __esm(() => {
|
|
|
17990
18003
|
|
|
17991
18004
|
// src/lib/assignee-validation.ts
|
|
17992
18005
|
import { readFileSync as readFileSync3 } from "fs";
|
|
17993
|
-
import { homedir as
|
|
18006
|
+
import { homedir as homedir3 } from "os";
|
|
17994
18007
|
import { join as join7 } from "path";
|
|
17995
18008
|
function defaultSeatRosterPath() {
|
|
17996
|
-
return process.env["TODOS_SEAT_ROSTER_PATH"] || join7(
|
|
18009
|
+
return process.env["TODOS_SEAT_ROSTER_PATH"] || join7(homedir3(), ".hasna", "identities", "hasna-seats.roster.json");
|
|
17997
18010
|
}
|
|
17998
18011
|
function loadSeatSlugs(path = defaultSeatRosterPath()) {
|
|
17999
18012
|
try {
|
|
@@ -20821,7 +20834,7 @@ async function validateMcpAssignee(value, allowSeat) {
|
|
|
20821
20834
|
return verdict.assignee;
|
|
20822
20835
|
}
|
|
20823
20836
|
function registerTaskCrudTools(server, ctx) {
|
|
20824
|
-
const { shouldRegisterTool, resolveId, formatError, formatTask } = ctx;
|
|
20837
|
+
const { shouldRegisterTool, resolveId, formatError, formatTask, applyFocus } = ctx;
|
|
20825
20838
|
function mutationTaskResponse(task) {
|
|
20826
20839
|
const compact = compactTask(task, 240);
|
|
20827
20840
|
compact["version"] = task.version;
|
|
@@ -20897,6 +20910,7 @@ function registerTaskCrudTools(server, ctx) {
|
|
|
20897
20910
|
payload.max_retries = retry_count;
|
|
20898
20911
|
if (deadline)
|
|
20899
20912
|
payload.due_at = deadline;
|
|
20913
|
+
applyFocus(payload, router.agent_id || undefined);
|
|
20900
20914
|
const created = await cloudCreateTask(cloud, payload);
|
|
20901
20915
|
return { content: [{ type: "text", text: mutationTaskResponse(created) }] };
|
|
20902
20916
|
}
|
|
@@ -20923,6 +20937,7 @@ function registerTaskCrudTools(server, ctx) {
|
|
|
20923
20937
|
resolved.max_retries = retry_count;
|
|
20924
20938
|
if (deadline)
|
|
20925
20939
|
resolved.due_at = deadline;
|
|
20940
|
+
applyFocus(resolved, router.agent_id || undefined);
|
|
20926
20941
|
const task = createTask(resolved);
|
|
20927
20942
|
return { content: [{ type: "text", text: mutationTaskResponse(task) }] };
|
|
20928
20943
|
} catch (e) {
|
|
@@ -26049,7 +26064,8 @@ __export(exports_saved_search_views, {
|
|
|
26049
26064
|
normalizeScope: () => normalizeScope,
|
|
26050
26065
|
listSearchViews: () => listSearchViews,
|
|
26051
26066
|
getSearchView: () => getSearchView,
|
|
26052
|
-
deleteSearchView: () => deleteSearchView
|
|
26067
|
+
deleteSearchView: () => deleteSearchView,
|
|
26068
|
+
SAVED_SEARCH_SCOPES: () => SAVED_SEARCH_SCOPES
|
|
26053
26069
|
});
|
|
26054
26070
|
function parseFilters(value) {
|
|
26055
26071
|
if (!value)
|
|
@@ -26069,10 +26085,8 @@ function rowToSavedSearchView(row) {
|
|
|
26069
26085
|
};
|
|
26070
26086
|
}
|
|
26071
26087
|
function normalizeScope(scope) {
|
|
26072
|
-
|
|
26073
|
-
|
|
26074
|
-
}
|
|
26075
|
-
return "tasks";
|
|
26088
|
+
const candidate = (scope ?? "").trim().toLowerCase();
|
|
26089
|
+
return SAVED_SEARCH_SCOPES.includes(candidate) ? candidate : "tasks";
|
|
26076
26090
|
}
|
|
26077
26091
|
function normalizeName4(name) {
|
|
26078
26092
|
const normalized = name.trim();
|
|
@@ -26375,10 +26389,12 @@ function runSearchView(idOrName, db) {
|
|
|
26375
26389
|
throw new Error(`Saved search view not found: ${idOrName}`);
|
|
26376
26390
|
return { ...runSavedSearch(view.filters, view.scope, d), view };
|
|
26377
26391
|
}
|
|
26392
|
+
var SAVED_SEARCH_SCOPES;
|
|
26378
26393
|
var init_saved_search_views = __esm(() => {
|
|
26379
26394
|
init_database();
|
|
26380
26395
|
init_local_fields();
|
|
26381
26396
|
init_search();
|
|
26397
|
+
SAVED_SEARCH_SCOPES = ["all", "tasks", "projects", "plans", "runs", "comments"];
|
|
26382
26398
|
});
|
|
26383
26399
|
|
|
26384
26400
|
// src/mcp/tools/task-project-tools.ts
|
|
@@ -30194,10 +30210,23 @@ function registerTaskAutoTools(server, ctx) {
|
|
|
30194
30210
|
return { content: [{ type: "text", text: "No active agents available for rebalancing." }] };
|
|
30195
30211
|
const activeTasks = listTasks3({ project_id: resolvedProjectId, status: ["pending", "in_progress"], limit: 1000 }, undefined);
|
|
30196
30212
|
const agentKeyByAlias = new Map;
|
|
30213
|
+
const ambiguousAgentAliases = new Set;
|
|
30214
|
+
const indexAgentAlias = (alias, agentId) => {
|
|
30215
|
+
const normalizedAlias2 = alias.toLowerCase();
|
|
30216
|
+
if (ambiguousAgentAliases.has(normalizedAlias2))
|
|
30217
|
+
return;
|
|
30218
|
+
const existingAgentId = agentKeyByAlias.get(normalizedAlias2);
|
|
30219
|
+
if (existingAgentId !== undefined && existingAgentId !== agentId) {
|
|
30220
|
+
agentKeyByAlias.delete(normalizedAlias2);
|
|
30221
|
+
ambiguousAgentAliases.add(normalizedAlias2);
|
|
30222
|
+
return;
|
|
30223
|
+
}
|
|
30224
|
+
agentKeyByAlias.set(normalizedAlias2, agentId);
|
|
30225
|
+
};
|
|
30197
30226
|
for (const agent of agents) {
|
|
30198
|
-
|
|
30227
|
+
indexAgentAlias(String(agent.id), agent.id);
|
|
30199
30228
|
if (agent.name)
|
|
30200
|
-
|
|
30229
|
+
indexAgentAlias(String(agent.name), agent.id);
|
|
30201
30230
|
}
|
|
30202
30231
|
const canonicalAgentKey = (assignedTo) => assignedTo ? agentKeyByAlias.get(assignedTo.toLowerCase()) : undefined;
|
|
30203
30232
|
const load = new Map(agents.map((agent) => [agent.id, activeTasks.filter((t) => canonicalAgentKey(t.assigned_to) === agent.id).length]));
|
|
@@ -34886,7 +34915,7 @@ var package_default;
|
|
|
34886
34915
|
var init_package = __esm(() => {
|
|
34887
34916
|
package_default = {
|
|
34888
34917
|
name: "@hasna/todos",
|
|
34889
|
-
version: "0.
|
|
34918
|
+
version: "0.15.1",
|
|
34890
34919
|
description: "Universal task management for AI coding agents - CLI + MCP server + interactive TUI",
|
|
34891
34920
|
type: "module",
|
|
34892
34921
|
main: "dist/index.js",
|
|
@@ -42865,9 +42894,16 @@ ${text2}` }] };
|
|
|
42865
42894
|
return { content: [{ type: "text", text: `Agent not found: ${id || name}` }], isError: true };
|
|
42866
42895
|
}
|
|
42867
42896
|
const oldName = agent.name;
|
|
42868
|
-
const updated = updateAgent(agent.id, { name: new_name });
|
|
42869
42897
|
const db = getDatabase();
|
|
42870
|
-
|
|
42898
|
+
let oldNameUniquelyIdentifiesAgent = false;
|
|
42899
|
+
try {
|
|
42900
|
+
oldNameUniquelyIdentifiesAgent = resolvePartialId(db, "agents", oldName) === agent.id;
|
|
42901
|
+
} catch (error) {
|
|
42902
|
+
if (!(error instanceof IdentityAliasAmbiguousError))
|
|
42903
|
+
throw error;
|
|
42904
|
+
}
|
|
42905
|
+
const updated = updateAgent(agent.id, { name: new_name });
|
|
42906
|
+
const tasksResult = db.run(oldNameUniquelyIdentifiesAgent ? "UPDATE tasks SET assigned_to = ? WHERE LOWER(assigned_to) = LOWER(?)" : "UPDATE tasks SET assigned_to = ? WHERE assigned_to = ?", [new_name, oldName]);
|
|
42871
42907
|
const taskNote = tasksResult.changes > 0 ? `
|
|
42872
42908
|
Updated assigned_to on ${tasksResult.changes} task(s).` : "";
|
|
42873
42909
|
return {
|
|
@@ -43051,6 +43087,7 @@ var init_agents2 = __esm(() => {
|
|
|
43051
43087
|
init_agents();
|
|
43052
43088
|
init_config2();
|
|
43053
43089
|
init_database();
|
|
43090
|
+
init_types();
|
|
43054
43091
|
init_cloud_router();
|
|
43055
43092
|
});
|
|
43056
43093
|
|
|
@@ -43819,7 +43856,7 @@ function captureEnvironmentSnapshot(input = {}) {
|
|
|
43819
43856
|
}
|
|
43820
43857
|
function writeEnvironmentSnapshot(snapshot, outputPath) {
|
|
43821
43858
|
const path = outputPath ? resolve15(outputPath) : join14(defaultSnapshotDir(), `${snapshot.id}.json`);
|
|
43822
|
-
|
|
43859
|
+
ensureDir(dirname8(path));
|
|
43823
43860
|
writeJsonFile(path, snapshot);
|
|
43824
43861
|
return path;
|
|
43825
43862
|
}
|
|
@@ -46459,9 +46496,11 @@ async function updateTask2(id, input, store) {
|
|
|
46459
46496
|
throw new Error(`Task ${id} version conflict: expected ${existing.version}, got ${input.version}`);
|
|
46460
46497
|
}
|
|
46461
46498
|
const reopened = existing.status === "completed" && input.status !== undefined && input.status !== "completed" && input.completed_at === undefined;
|
|
46499
|
+
const terminalNow = input.status !== undefined && isTerminalStatus(input.status);
|
|
46462
46500
|
const task2 = {
|
|
46463
46501
|
...existing,
|
|
46464
46502
|
...definedPatch(input),
|
|
46503
|
+
...terminalNow ? { locked_by: null, locked_at: null } : {},
|
|
46465
46504
|
version: existing.version + 1,
|
|
46466
46505
|
updated_at: new Date().toISOString(),
|
|
46467
46506
|
tags: input.tags ?? existing.tags,
|
|
@@ -48034,6 +48073,103 @@ var init_auth_posture = __esm(() => {
|
|
|
48034
48073
|
LOOPBACK_HOSTNAMES = new Set(["localhost", "127.0.0.1", "::1", "[::1]", "ip6-localhost"]);
|
|
48035
48074
|
});
|
|
48036
48075
|
|
|
48076
|
+
// src/lib/enum-vocabulary.ts
|
|
48077
|
+
function editDistance(a, b) {
|
|
48078
|
+
if (a === b)
|
|
48079
|
+
return 0;
|
|
48080
|
+
if (!a.length)
|
|
48081
|
+
return b.length;
|
|
48082
|
+
if (!b.length)
|
|
48083
|
+
return a.length;
|
|
48084
|
+
let previous = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
48085
|
+
for (let i = 1;i <= a.length; i += 1) {
|
|
48086
|
+
const current = [i];
|
|
48087
|
+
for (let j = 1;j <= b.length; j += 1) {
|
|
48088
|
+
current[j] = Math.min(previous[j] + 1, current[j - 1] + 1, previous[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
48089
|
+
}
|
|
48090
|
+
previous = current;
|
|
48091
|
+
}
|
|
48092
|
+
return previous[b.length];
|
|
48093
|
+
}
|
|
48094
|
+
function suggestVocabularyMatches(value, vocabulary, limit = 3) {
|
|
48095
|
+
const needle = value.trim().toLowerCase();
|
|
48096
|
+
if (!needle)
|
|
48097
|
+
return [];
|
|
48098
|
+
const scored = [];
|
|
48099
|
+
for (const member of vocabulary) {
|
|
48100
|
+
const candidate = member.toLowerCase();
|
|
48101
|
+
if (candidate.startsWith(needle) || needle.startsWith(candidate)) {
|
|
48102
|
+
scored.push({ member, score: 0 });
|
|
48103
|
+
continue;
|
|
48104
|
+
}
|
|
48105
|
+
if (candidate.includes(needle) || needle.includes(candidate)) {
|
|
48106
|
+
scored.push({ member, score: 1 });
|
|
48107
|
+
continue;
|
|
48108
|
+
}
|
|
48109
|
+
const distance = editDistance(needle, candidate);
|
|
48110
|
+
if (distance <= Math.max(1, Math.floor(candidate.length / 3))) {
|
|
48111
|
+
scored.push({ member, score: 1 + distance });
|
|
48112
|
+
}
|
|
48113
|
+
}
|
|
48114
|
+
return scored.sort((a, b) => a.score - b.score || a.member.localeCompare(b.member)).slice(0, limit).map((entry2) => entry2.member);
|
|
48115
|
+
}
|
|
48116
|
+
function resolveEnumVocabulary(raw, spec) {
|
|
48117
|
+
const allowList = spec.allowList !== false;
|
|
48118
|
+
const rawElements = (allowList ? raw.split(",") : [raw]).map((element) => element.trim());
|
|
48119
|
+
if (rawElements.every((element) => element.length === 0)) {
|
|
48120
|
+
return {
|
|
48121
|
+
ok: false,
|
|
48122
|
+
message: `${spec.name} requires a value. Allowed values: ${spec.vocabulary.join(", ")}.`,
|
|
48123
|
+
invalid: []
|
|
48124
|
+
};
|
|
48125
|
+
}
|
|
48126
|
+
if (rawElements.some((element) => element.length === 0)) {
|
|
48127
|
+
return {
|
|
48128
|
+
ok: false,
|
|
48129
|
+
message: `${spec.name} has an empty value in "${raw}" \u2014 remove the stray comma. ` + `Allowed values: ${spec.vocabulary.join(", ")}.`,
|
|
48130
|
+
invalid: []
|
|
48131
|
+
};
|
|
48132
|
+
}
|
|
48133
|
+
const normalized = rawElements.map((element) => spec.normalize ? spec.normalize(element) : element);
|
|
48134
|
+
const allowed = new Set(spec.vocabulary);
|
|
48135
|
+
const invalid2 = [];
|
|
48136
|
+
for (let i = 0;i < normalized.length; i += 1) {
|
|
48137
|
+
if (!allowed.has(normalized[i]))
|
|
48138
|
+
invalid2.push(rawElements[i]);
|
|
48139
|
+
}
|
|
48140
|
+
if (invalid2.length === 0) {
|
|
48141
|
+
return { ok: true, values: [...new Set(normalized)] };
|
|
48142
|
+
}
|
|
48143
|
+
const label = invalid2.length === 1 ? "value" : "values";
|
|
48144
|
+
const parts = [
|
|
48145
|
+
`Invalid ${spec.name} ${label}: ${invalid2.join(", ")}.`,
|
|
48146
|
+
`Allowed values: ${spec.vocabulary.join(", ")}.`
|
|
48147
|
+
];
|
|
48148
|
+
const hints = new Set;
|
|
48149
|
+
const suggestions = new Set;
|
|
48150
|
+
for (let i = 0;i < normalized.length; i += 1) {
|
|
48151
|
+
const canonical = normalized[i];
|
|
48152
|
+
if (allowed.has(canonical))
|
|
48153
|
+
continue;
|
|
48154
|
+
const hint = spec.hints?.[canonical.toLowerCase()];
|
|
48155
|
+
if (hint) {
|
|
48156
|
+
hints.add(hint);
|
|
48157
|
+
continue;
|
|
48158
|
+
}
|
|
48159
|
+
for (const match of suggestVocabularyMatches(canonical, spec.vocabulary)) {
|
|
48160
|
+
suggestions.add(match);
|
|
48161
|
+
}
|
|
48162
|
+
}
|
|
48163
|
+
if (suggestions.size > 0)
|
|
48164
|
+
parts.push(`Did you mean ${[...suggestions].join(", ")}?`);
|
|
48165
|
+
for (const hint of hints)
|
|
48166
|
+
parts.push(hint);
|
|
48167
|
+
return { ok: false, message: parts.join(" "), invalid: invalid2 };
|
|
48168
|
+
}
|
|
48169
|
+
function collapseEnumValues(values) {
|
|
48170
|
+
return values.length === 1 ? values[0] : values;
|
|
48171
|
+
}
|
|
48172
|
+
|
|
48037
48173
|
// src/db/orgs.ts
|
|
48038
48174
|
function rowToOrg(row) {
|
|
48039
48175
|
return { ...row, metadata: JSON.parse(row.metadata || "{}") };
|
|
@@ -48334,8 +48470,19 @@ function handleStats(_ctx, json2) {
|
|
|
48334
48470
|
recurring_tasks: countRecurringTasks()
|
|
48335
48471
|
});
|
|
48336
48472
|
}
|
|
48473
|
+
function taskStatusQueryParam(url) {
|
|
48474
|
+
const raw = url.searchParams.get("status");
|
|
48475
|
+
if (!raw)
|
|
48476
|
+
return { ok: true, value: undefined };
|
|
48477
|
+
const result = resolveEnumVocabulary(raw, { name: "status", vocabulary: TASK_STATUSES });
|
|
48478
|
+
if (!result.ok)
|
|
48479
|
+
return { ok: false, message: result.message };
|
|
48480
|
+
return { ok: true, value: collapseEnumValues(result.values) };
|
|
48481
|
+
}
|
|
48337
48482
|
async function handleListTasks(_req, url, _ctx, json2, taskToSummary2) {
|
|
48338
|
-
const
|
|
48483
|
+
const statusParam = taskStatusQueryParam(url);
|
|
48484
|
+
if (!statusParam.ok)
|
|
48485
|
+
return json2({ error: statusParam.message }, 400);
|
|
48339
48486
|
const projectId = url.searchParams.get("project_id") || undefined;
|
|
48340
48487
|
const sessionId = url.searchParams.get("session_id") || undefined;
|
|
48341
48488
|
const agentId = url.searchParams.get("agent_id") || undefined;
|
|
@@ -48343,7 +48490,7 @@ async function handleListTasks(_req, url, _ctx, json2, taskToSummary2) {
|
|
|
48343
48490
|
const offsetParam = url.searchParams.get("offset");
|
|
48344
48491
|
const fields = parseFieldsParam(url);
|
|
48345
48492
|
const tasks = listTasks({
|
|
48346
|
-
status,
|
|
48493
|
+
status: statusParam.value,
|
|
48347
48494
|
project_id: projectId,
|
|
48348
48495
|
session_id: sessionId,
|
|
48349
48496
|
agent_id: agentId,
|
|
@@ -48408,9 +48555,19 @@ async function handleUpsertTask(req, ctx, json2, taskToSummary2) {
|
|
|
48408
48555
|
}
|
|
48409
48556
|
function handleTasksExport(_req, url, _ctx, _json, taskToSummary2) {
|
|
48410
48557
|
const format = url.searchParams.get("format") || "json";
|
|
48411
|
-
const
|
|
48558
|
+
const statusParam = taskStatusQueryParam(url);
|
|
48559
|
+
if (!statusParam.ok) {
|
|
48560
|
+
return new Response(JSON.stringify({ error: statusParam.message }), {
|
|
48561
|
+
status: 400,
|
|
48562
|
+
headers: { "Content-Type": "application/json" }
|
|
48563
|
+
});
|
|
48564
|
+
}
|
|
48412
48565
|
const projectId = url.searchParams.get("project_id") || undefined;
|
|
48413
|
-
const tasks = listTasks({
|
|
48566
|
+
const tasks = listTasks({
|
|
48567
|
+
status: statusParam.value,
|
|
48568
|
+
project_id: projectId,
|
|
48569
|
+
limit: 1e4
|
|
48570
|
+
});
|
|
48414
48571
|
const summaries = tasks.map((t) => taskToSummary2(t));
|
|
48415
48572
|
if (format === "csv") {
|
|
48416
48573
|
const headers = ["id", "short_id", "title", "status", "priority", "project_id", "assigned_to", "agent_id", "created_at", "updated_at", "completed_at", "due_at"];
|
|
@@ -48992,6 +49149,7 @@ var init_routes = __esm(() => {
|
|
|
48992
49149
|
init_tasks();
|
|
48993
49150
|
init_database();
|
|
48994
49151
|
init_types();
|
|
49152
|
+
init_types();
|
|
48995
49153
|
init_projects();
|
|
48996
49154
|
init_agents();
|
|
48997
49155
|
init_plans();
|
|
@@ -49945,8 +50103,38 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
49945
50103
|
operationId: "listTasks",
|
|
49946
50104
|
summary: "List tasks",
|
|
49947
50105
|
parameters: [
|
|
49948
|
-
{
|
|
49949
|
-
|
|
50106
|
+
{
|
|
50107
|
+
name: "status",
|
|
50108
|
+
in: "query",
|
|
50109
|
+
description: `Task status, or a comma-separated list of statuses. Allowed values: ${TASK_STATUSES.join(", ")}.`,
|
|
50110
|
+
style: "form",
|
|
50111
|
+
explode: false,
|
|
50112
|
+
schema: {
|
|
50113
|
+
oneOf: [
|
|
50114
|
+
{ type: "string", enum: [...TASK_STATUSES] },
|
|
50115
|
+
{
|
|
50116
|
+
type: "array",
|
|
50117
|
+
items: { type: "string", enum: [...TASK_STATUSES] }
|
|
50118
|
+
}
|
|
50119
|
+
]
|
|
50120
|
+
}
|
|
50121
|
+
},
|
|
50122
|
+
{
|
|
50123
|
+
name: "priority",
|
|
50124
|
+
in: "query",
|
|
50125
|
+
description: `Task priority, or a comma-separated list of priorities. Allowed values: ${TASK_PRIORITIES.join(", ")}.`,
|
|
50126
|
+
style: "form",
|
|
50127
|
+
explode: false,
|
|
50128
|
+
schema: {
|
|
50129
|
+
oneOf: [
|
|
50130
|
+
{ type: "string", enum: [...TASK_PRIORITIES] },
|
|
50131
|
+
{
|
|
50132
|
+
type: "array",
|
|
50133
|
+
items: { type: "string", enum: [...TASK_PRIORITIES] }
|
|
50134
|
+
}
|
|
50135
|
+
]
|
|
50136
|
+
}
|
|
50137
|
+
},
|
|
49950
50138
|
{ name: "project_id", in: "query", schema: { type: "string" } },
|
|
49951
50139
|
{ name: "parent_id", in: "query", schema: { type: "string", nullable: true } },
|
|
49952
50140
|
{ name: "include_subtasks", in: "query", schema: { type: "boolean" } },
|
|
@@ -50458,6 +50646,7 @@ function buildV1OpenApiDocument(version = getPackageVersion()) {
|
|
|
50458
50646
|
var taskSchema, projectSchema, taskListSchema, taskCommentSchema, planSchema, templateTaskSchema, templateSchema, templateVariableSchema, createTemplateTaskInputSchema;
|
|
50459
50647
|
var init_openapi = __esm(() => {
|
|
50460
50648
|
init_package_version();
|
|
50649
|
+
init_types();
|
|
50461
50650
|
taskSchema = {
|
|
50462
50651
|
type: "object",
|
|
50463
50652
|
properties: {
|
|
@@ -50725,6 +50914,15 @@ function json3(body, status = 200) {
|
|
|
50725
50914
|
function error(status, message, extra) {
|
|
50726
50915
|
return json3({ error: message, ...extra ?? {} }, status);
|
|
50727
50916
|
}
|
|
50917
|
+
function enumQueryParam(url, name, vocabulary) {
|
|
50918
|
+
const raw = url.searchParams.get(name);
|
|
50919
|
+
if (raw === null || raw === "")
|
|
50920
|
+
return { ok: true, value: undefined };
|
|
50921
|
+
const result = resolveEnumVocabulary(raw, { name, vocabulary });
|
|
50922
|
+
if (!result.ok)
|
|
50923
|
+
return { ok: false, response: error(400, result.message) };
|
|
50924
|
+
return { ok: true, value: collapseEnumValues(result.values) };
|
|
50925
|
+
}
|
|
50728
50926
|
function validateTaskCompletion(value) {
|
|
50729
50927
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
50730
50928
|
return { ok: false, message: "completion body must be an object" };
|
|
@@ -51145,14 +51343,16 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
51145
51343
|
return error(400, "include_subtasks must be true or false");
|
|
51146
51344
|
}
|
|
51147
51345
|
const hasParentFilter = url.searchParams.has("parent_id");
|
|
51346
|
+
const statusParam = enumQueryParam(url, "status", TASK_STATUSES);
|
|
51347
|
+
if (!statusParam.ok)
|
|
51348
|
+
return statusParam.response;
|
|
51349
|
+
const priorityParam = enumQueryParam(url, "priority", TASK_PRIORITIES);
|
|
51350
|
+
if (!priorityParam.ok)
|
|
51351
|
+
return priorityParam.response;
|
|
51148
51352
|
const filter = {
|
|
51149
51353
|
...url.searchParams.get("q") ? { query: url.searchParams.get("q") } : {},
|
|
51150
|
-
...
|
|
51151
|
-
|
|
51152
|
-
} : {},
|
|
51153
|
-
...url.searchParams.get("priority") ? {
|
|
51154
|
-
priority: url.searchParams.get("priority").includes(",") ? url.searchParams.get("priority").split(",") : url.searchParams.get("priority")
|
|
51155
|
-
} : {},
|
|
51354
|
+
...statusParam.value !== undefined ? { status: statusParam.value } : {},
|
|
51355
|
+
...priorityParam.value !== undefined ? { priority: priorityParam.value } : {},
|
|
51156
51356
|
...url.searchParams.get("project_id") ? { project_id: url.searchParams.get("project_id") } : {},
|
|
51157
51357
|
...hasParentFilter ? { parent_id: url.searchParams.get("parent_id") || null, include_subtasks: true } : includeSubtasks !== null ? { include_subtasks: includeSubtasks === "true" } : {},
|
|
51158
51358
|
...url.searchParams.get("plan_id") ? { plan_id: url.searchParams.get("plan_id") } : {},
|
|
@@ -51786,7 +51986,13 @@ async function handleV1Request(req, url, dependencies = {}) {
|
|
|
51786
51986
|
return error(405, `method ${method} not allowed on /v1/refs/:ref`);
|
|
51787
51987
|
if (!store.gitRefs)
|
|
51788
51988
|
return error(501, "git ref links are not supported by this storage backend");
|
|
51789
|
-
|
|
51989
|
+
let decodedRef;
|
|
51990
|
+
try {
|
|
51991
|
+
decodedRef = decodeURIComponent(id);
|
|
51992
|
+
} catch {
|
|
51993
|
+
return error(400, "ref path segment has invalid percent encoding");
|
|
51994
|
+
}
|
|
51995
|
+
const refs = await store.gitRefs.find(decodedRef);
|
|
51790
51996
|
return json3({ refs, count: refs.length });
|
|
51791
51997
|
}
|
|
51792
51998
|
if (resource === "next" && !id) {
|
|
@@ -52892,7 +53098,8 @@ function buildServer() {
|
|
|
52892
53098
|
formatError,
|
|
52893
53099
|
formatTask,
|
|
52894
53100
|
formatTaskDetail,
|
|
52895
|
-
getAgentFocus
|
|
53101
|
+
getAgentFocus,
|
|
53102
|
+
applyFocus
|
|
52896
53103
|
};
|
|
52897
53104
|
registerTaskCrudTools(server, toolContext);
|
|
52898
53105
|
registerTaskProjectTools(server, toolContext);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../../../src/mcp/tools/agents.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;
|
|
1
|
+
{"version":3,"file":"agents.d.ts","sourceRoot":"","sources":["../../../src/mcp/tools/agents.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAQzE,UAAU,UAAU;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,KAAK,OAAO,GAAG;IACb,kBAAkB,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;IAC9C,SAAS,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IAClD,WAAW,EAAE,CAAC,CAAC,EAAE,OAAO,KAAK,MAAM,CAAC;IACpC,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IACvC,aAAa,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,UAAU,GAAG,SAAS,CAAC;CAC5D,CAAC;AAEF,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,kBAAkB,EAAE,SAAS,EAAE,WAAW,EAAE,aAAa,EAAE,aAAa,EAAE,EAAE,OAAO,GAAG,IAAI,CA4fjJ"}
|