@agentprojectcontext/apx 1.74.2 → 1.76.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/package.json +1 -1
- package/src/core/agent/prompt-builder.js +26 -7
- package/src/core/agent/render-template.js +22 -0
- package/src/core/profiles/block.js +290 -0
- package/src/core/profiles/bundled/secretary/PROFILE.es.md +44 -0
- package/src/core/profiles/bundled/secretary/PROFILE.md +44 -0
- package/src/core/profiles/bundled/secretary/channels/routine.md +43 -0
- package/src/core/profiles/bundled/secretary/config.schema.json +49 -0
- package/src/core/profiles/bundled/secretary/profile.json +20 -0
- package/src/core/profiles/bundled/secretary/routines/day-close.json +10 -0
- package/src/core/profiles/bundled/secretary/routines/day-open.json +10 -0
- package/src/core/profiles/index.js +16 -0
- package/src/core/profiles/lifecycle.js +720 -0
- package/src/core/profiles/manifest.js +193 -0
- package/src/core/profiles/paths.js +51 -0
- package/src/core/profiles/store.js +184 -0
- package/src/core/runtime-skills/apx-profile/SKILL.md +126 -0
- package/src/core/runtime-skills/apx-task/SKILL.md +4 -0
- package/src/core/stores/routines.js +9 -1
- package/src/core/stores/tasks.js +70 -1
- package/src/host/daemon/api/profiles.js +179 -0
- package/src/host/daemon/api/tasks.js +36 -15
- package/src/host/daemon/api/web.js +1 -1
- package/src/host/daemon/api.js +2 -0
- package/src/interfaces/cli/commands/profile.js +252 -0
- package/src/interfaces/cli/commands/task.js +44 -13
- package/src/interfaces/cli/index.js +69 -1
- package/src/interfaces/web/dist/assets/{index-CQTIGYCu.js → index-Bjlk9ttU.js} +165 -160
- package/src/interfaces/web/dist/assets/index-Bjlk9ttU.js.map +1 -0
- package/src/interfaces/web/dist/assets/index-CQ5kyFej.css +1 -0
- package/src/interfaces/web/dist/index.html +2 -2
- package/src/interfaces/web/src/components/settings/ProfilePanel.tsx +245 -0
- package/src/interfaces/web/src/hooks/useProfiles.ts +37 -0
- package/src/interfaces/web/src/i18n/en.ts +32 -0
- package/src/interfaces/web/src/i18n/es.ts +32 -0
- package/src/interfaces/web/src/lib/api/profiles.ts +86 -0
- package/src/interfaces/web/src/lib/api/tasks.ts +6 -2
- package/src/interfaces/web/src/screens/SettingsScreen.tsx +6 -2
- package/src/interfaces/web/src/screens/base/GlobalTasksTab.tsx +21 -3
- package/src/interfaces/web/dist/assets/index-COrRuBp1.css +0 -1
- package/src/interfaces/web/dist/assets/index-CQTIGYCu.js.map +0 -1
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
// GET /profiles installed + bundled packages, which one is active
|
|
2
|
+
// GET /profiles/doctor health of the active profile (or ?id=)
|
|
3
|
+
// GET /profiles/:id one package, with its schema, settings and prompt preview
|
|
4
|
+
// POST /profiles/install { source, force? }
|
|
5
|
+
// POST /profiles/use { id, force? }
|
|
6
|
+
// POST /profiles/off
|
|
7
|
+
// PATCH /profiles/config { values: {...}, id? }
|
|
8
|
+
// DELETE /profiles/:id uninstall
|
|
9
|
+
//
|
|
10
|
+
// Thin adapter — body → core/profiles → response. The daemon is the writer on
|
|
11
|
+
// purpose: activating a profile changes the live system prompt and the routine
|
|
12
|
+
// schedule, so the process that owns both has to be the one applying it.
|
|
13
|
+
import { readConfig } from "#core/config/index.js";
|
|
14
|
+
import { readIdentity } from "#core/identity/index.js";
|
|
15
|
+
import {
|
|
16
|
+
listProfilesWithState,
|
|
17
|
+
readProfile,
|
|
18
|
+
readProfileState,
|
|
19
|
+
effectiveProfileConfig,
|
|
20
|
+
installProfile,
|
|
21
|
+
useProfile,
|
|
22
|
+
offProfile,
|
|
23
|
+
setProfileConfig,
|
|
24
|
+
uninstallProfile,
|
|
25
|
+
profileDoctor,
|
|
26
|
+
renderProfilePrompt,
|
|
27
|
+
estimateTokens,
|
|
28
|
+
} from "#core/profiles/index.js";
|
|
29
|
+
|
|
30
|
+
/** 400 for anything the caller could have got right, 500 for the rest. */
|
|
31
|
+
function fail(res, e) {
|
|
32
|
+
const msg = e?.message || String(e);
|
|
33
|
+
const isUserError =
|
|
34
|
+
/not installed|not found|invalid|unknown setting|must be|already|missing|required|not supported|cannot be activated|failed:/i.test(
|
|
35
|
+
msg
|
|
36
|
+
);
|
|
37
|
+
res.status(isUserError ? 400 : 500).json({ error: msg });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function detail(id, { preview = true } = {}) {
|
|
41
|
+
const profile = readProfile(id);
|
|
42
|
+
if (!profile) return null;
|
|
43
|
+
|
|
44
|
+
const cfg = readConfig();
|
|
45
|
+
const state = readProfileState(cfg);
|
|
46
|
+
const identity = (() => { try { return readIdentity(); } catch { return null; } })();
|
|
47
|
+
const settings = effectiveProfileConfig(profile, cfg);
|
|
48
|
+
|
|
49
|
+
const languages = profile.prompts.map((f) => {
|
|
50
|
+
const m = f.match(/^PROFILE\.([\w-]+)\.md$/);
|
|
51
|
+
return m ? m[1] : "en";
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const rendered = preview
|
|
55
|
+
? renderProfilePrompt(profile, {
|
|
56
|
+
identity,
|
|
57
|
+
globalConfig: { ...cfg, profile: { active: id, config: settings } },
|
|
58
|
+
lang: cfg?.user?.language || identity?.language || "en",
|
|
59
|
+
})
|
|
60
|
+
: "";
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
id: profile.id,
|
|
64
|
+
name: profile.manifest.name || profile.id,
|
|
65
|
+
version: profile.manifest.version || null,
|
|
66
|
+
description: profile.manifest.description || "",
|
|
67
|
+
author: profile.manifest.author || null,
|
|
68
|
+
source: profile.source,
|
|
69
|
+
dir: profile.dir,
|
|
70
|
+
active: state.active === id,
|
|
71
|
+
languages: [...new Set(languages)].sort(),
|
|
72
|
+
provides: profile.manifest.provides || {},
|
|
73
|
+
requires: profile.manifest.requires || {},
|
|
74
|
+
schema: profile.schema || null,
|
|
75
|
+
defaults: profile.defaults,
|
|
76
|
+
config: settings,
|
|
77
|
+
budget: profile.manifest.prompt_budget_tokens || null,
|
|
78
|
+
tokens: preview ? estimateTokens(rendered) : null,
|
|
79
|
+
// The rendered block, exactly as it reaches the model. This is the best
|
|
80
|
+
// debugging tool the panel can offer and it costs nothing to expose.
|
|
81
|
+
preview: rendered,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function register(app) {
|
|
86
|
+
app.get("/profiles", (_req, res) => {
|
|
87
|
+
try {
|
|
88
|
+
const cfg = readConfig();
|
|
89
|
+
res.json({
|
|
90
|
+
active: readProfileState(cfg).active,
|
|
91
|
+
profiles: listProfilesWithState(cfg),
|
|
92
|
+
});
|
|
93
|
+
} catch (e) {
|
|
94
|
+
fail(res, e);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Registered before /profiles/:id so "doctor" isn't swallowed as an id.
|
|
99
|
+
app.get("/profiles/doctor", (req, res) => {
|
|
100
|
+
try {
|
|
101
|
+
res.json(profileDoctor(req.query?.id || null));
|
|
102
|
+
} catch (e) {
|
|
103
|
+
fail(res, e);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
app.get("/profiles/:id", (req, res) => {
|
|
108
|
+
try {
|
|
109
|
+
const out = detail(req.params.id, { preview: req.query?.preview !== "0" });
|
|
110
|
+
if (!out) return res.status(404).json({ error: `profile "${req.params.id}" not found` });
|
|
111
|
+
res.json(out);
|
|
112
|
+
} catch (e) {
|
|
113
|
+
fail(res, e);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
app.post("/profiles/install", (req, res) => {
|
|
118
|
+
try {
|
|
119
|
+
const { source, force } = req.body || {};
|
|
120
|
+
if (!source) return res.status(400).json({ error: "body needs { source }" });
|
|
121
|
+
const out = installProfile(source, { force: !!force });
|
|
122
|
+
res.json({
|
|
123
|
+
ok: true,
|
|
124
|
+
profile: detail(out.profile.id),
|
|
125
|
+
warnings: out.warnings,
|
|
126
|
+
tokens: out.tokens,
|
|
127
|
+
doctor: out.doctor,
|
|
128
|
+
});
|
|
129
|
+
} catch (e) {
|
|
130
|
+
fail(res, e);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
app.post("/profiles/use", (req, res) => {
|
|
135
|
+
try {
|
|
136
|
+
const { id, force } = req.body || {};
|
|
137
|
+
if (!id) return res.status(400).json({ error: "body needs { id }" });
|
|
138
|
+
const out = useProfile(id, { confirmReplace: !!force });
|
|
139
|
+
res.json({
|
|
140
|
+
ok: true,
|
|
141
|
+
profile: detail(id),
|
|
142
|
+
routines: out.routines,
|
|
143
|
+
warnings: out.warnings,
|
|
144
|
+
tokens: out.tokens,
|
|
145
|
+
});
|
|
146
|
+
} catch (e) {
|
|
147
|
+
fail(res, e);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
app.post("/profiles/off", (_req, res) => {
|
|
152
|
+
try {
|
|
153
|
+
res.json({ ok: true, ...offProfile() });
|
|
154
|
+
} catch (e) {
|
|
155
|
+
fail(res, e);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
app.patch("/profiles/config", (req, res) => {
|
|
160
|
+
try {
|
|
161
|
+
const { values, id } = req.body || {};
|
|
162
|
+
if (!values || typeof values !== "object") {
|
|
163
|
+
return res.status(400).json({ error: "body needs { values: { key: value } }" });
|
|
164
|
+
}
|
|
165
|
+
const out = setProfileConfig(values, { id: id || null });
|
|
166
|
+
res.json({ ok: true, ...out });
|
|
167
|
+
} catch (e) {
|
|
168
|
+
fail(res, e);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
app.delete("/profiles/:id", (req, res) => {
|
|
173
|
+
try {
|
|
174
|
+
res.json({ ok: true, ...uninstallProfile(req.params.id) });
|
|
175
|
+
} catch (e) {
|
|
176
|
+
fail(res, e);
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
//
|
|
2
|
-
// GET /
|
|
1
|
+
// Tasks (TODOs). Backed by core/stores/tasks.js (JSONL event log).
|
|
2
|
+
// GET /tasks cross-project; same filters, plus ?offset
|
|
3
|
+
// GET /projects/:pid/tasks ?state=open|done|dropped|all&tag=X&agent=Y
|
|
4
|
+
// &due_before=ISO&due_after=ISO&limit=N
|
|
5
|
+
// &status=pending|running|in_review|blocked&updated_since=ISO
|
|
3
6
|
// POST /projects/:pid/tasks { title, body?, tags?, due?, agent?, source?, meta? }
|
|
4
7
|
// GET /projects/:pid/tasks/:id (id or prefix)
|
|
5
8
|
// PATCH /projects/:pid/tasks/:id { patch: {...} }
|
|
@@ -9,6 +12,7 @@
|
|
|
9
12
|
import {
|
|
10
13
|
createTask,
|
|
11
14
|
listTasks,
|
|
15
|
+
listTasksAcrossProjects,
|
|
12
16
|
getTask,
|
|
13
17
|
patchTask,
|
|
14
18
|
doneTask,
|
|
@@ -25,21 +29,36 @@ export function register(app, { project, projects }) {
|
|
|
25
29
|
// envelope. Paginated via ?limit & ?offset; with no limit, data is the full
|
|
26
30
|
// set as one page.
|
|
27
31
|
app.get("/tasks", (req, res) => {
|
|
28
|
-
const state = req.query
|
|
29
|
-
|
|
32
|
+
const { state, tag, agent, due_before, due_after, status, updated_since } = req.query;
|
|
33
|
+
|
|
34
|
+
// Resolve the registered projects to what core needs, dropping any the
|
|
35
|
+
// manager can no longer open.
|
|
36
|
+
const entries = [];
|
|
30
37
|
for (const entry of projects.list()) {
|
|
31
38
|
const p = projects.get(entry.id);
|
|
32
|
-
if (!p) continue;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
for (const t of tasks) out.push({ ...t, project_id: entry.id, project_name: entry.name || entry.path });
|
|
39
|
+
if (!p?.storagePath) continue;
|
|
40
|
+
entries.push({
|
|
41
|
+
id: entry.id,
|
|
42
|
+
name: entry.name || entry.path,
|
|
43
|
+
path: entry.path,
|
|
44
|
+
storagePath: p.storagePath,
|
|
45
|
+
});
|
|
40
46
|
}
|
|
41
|
-
|
|
42
|
-
|
|
47
|
+
|
|
48
|
+
const { tasks, skipped } = listTasksAcrossProjects(entries, {
|
|
49
|
+
state: state === "all" ? undefined : (state || "open"),
|
|
50
|
+
tag: tag || undefined,
|
|
51
|
+
agent: agent || undefined,
|
|
52
|
+
due_before: due_before || undefined,
|
|
53
|
+
due_after: due_after || undefined,
|
|
54
|
+
status: status || undefined,
|
|
55
|
+
updated_since: updated_since || undefined,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const envelope = pageEnvelope(tasks, req.query);
|
|
59
|
+
// Say when a project could not be read rather than quietly showing less.
|
|
60
|
+
if (skipped.length) envelope.meta = { ...(envelope.meta || {}), skipped };
|
|
61
|
+
res.json(envelope);
|
|
43
62
|
});
|
|
44
63
|
|
|
45
64
|
// Per-project tasks. Returns a { meta, data } envelope; with no ?limit the
|
|
@@ -47,13 +66,15 @@ export function register(app, { project, projects }) {
|
|
|
47
66
|
app.get("/projects/:pid/tasks", (req, res) => {
|
|
48
67
|
const p = project(req, res);
|
|
49
68
|
if (!p) return;
|
|
50
|
-
const { state, tag, agent, due_before, due_after } = req.query;
|
|
69
|
+
const { state, tag, agent, due_before, due_after, status, updated_since } = req.query;
|
|
51
70
|
const all = listTasks(p.storagePath, {
|
|
52
71
|
state: state || undefined,
|
|
53
72
|
tag: tag || undefined,
|
|
54
73
|
agent: agent || undefined,
|
|
55
74
|
due_before: due_before || undefined,
|
|
56
75
|
due_after: due_after || undefined,
|
|
76
|
+
status: status || undefined,
|
|
77
|
+
updated_since: updated_since || undefined,
|
|
57
78
|
});
|
|
58
79
|
res.json(pageEnvelope(all, req.query));
|
|
59
80
|
});
|
|
@@ -24,7 +24,7 @@ const API_PREFIXES = [
|
|
|
24
24
|
"/health", "/admin", "/projects", "/telegram", "/engines", "/runtimes",
|
|
25
25
|
"/messages", "/sessions", "/tools", "/mcp", "/voice", "/tts", "/desktop", "/overlay",
|
|
26
26
|
"/transcribe", "/run", "/files", "/memory", "/env", "/pair", "/deck",
|
|
27
|
-
"/super-agent", "/identity", "/skills",
|
|
27
|
+
"/super-agent", "/identity", "/skills", "/profiles",
|
|
28
28
|
];
|
|
29
29
|
|
|
30
30
|
export function isApiPath(p) {
|
package/src/host/daemon/api.js
CHANGED
|
@@ -51,6 +51,7 @@ import { register as registerPairing } from "./api/pairing.js";
|
|
|
51
51
|
import { register as registerAdmin } from "./api/admin.js";
|
|
52
52
|
import { register as registerAdminConfig } from "./api/admin-config.js";
|
|
53
53
|
import { register as registerIdentity } from "./api/identity.js";
|
|
54
|
+
import { register as registerProfiles } from "./api/profiles.js";
|
|
54
55
|
import { register as registerWeb } from "./api/web.js";
|
|
55
56
|
import { register as registerConfirm } from "./api/confirm.js";
|
|
56
57
|
|
|
@@ -151,6 +152,7 @@ export function buildApi({
|
|
|
151
152
|
registerAdmin(app, ctx);
|
|
152
153
|
registerAdminConfig(app, ctx);
|
|
153
154
|
registerIdentity(app, ctx);
|
|
155
|
+
registerProfiles(app, ctx);
|
|
154
156
|
|
|
155
157
|
// ---- Web admin panel (static SPA, must mount before 404) ---------
|
|
156
158
|
// Serves src/interfaces/web/dist when present + the /admin/web-token
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
// apx profile — install, activate and configure the super-agent's personality.
|
|
2
|
+
//
|
|
3
|
+
// apx profile list
|
|
4
|
+
// apx profile show <id> [--preview]
|
|
5
|
+
// apx profile install <id|path> [--force]
|
|
6
|
+
// apx profile use <id> [--force]
|
|
7
|
+
// apx profile off
|
|
8
|
+
// apx profile config [--set k=v]... [--interactive]
|
|
9
|
+
// apx profile doctor [<id>]
|
|
10
|
+
// apx profile uninstall <id>
|
|
11
|
+
//
|
|
12
|
+
// Everything goes through the daemon: activating a profile changes the live
|
|
13
|
+
// system prompt and rewrites the routine schedule, so the process that owns
|
|
14
|
+
// both has to apply it. Writing config from here would leave the running
|
|
15
|
+
// daemon out of date.
|
|
16
|
+
import readline from "node:readline/promises";
|
|
17
|
+
import { http } from "../http.js";
|
|
18
|
+
|
|
19
|
+
export const PROFILE_USAGE = {
|
|
20
|
+
list: "apx profile list",
|
|
21
|
+
show: "apx profile show <id> [--preview]",
|
|
22
|
+
install: "apx profile install <id|path> [--force]",
|
|
23
|
+
use: "apx profile use <id> [--force]",
|
|
24
|
+
off: "apx profile off",
|
|
25
|
+
config: "apx profile config [--set key=value]... [--interactive]",
|
|
26
|
+
doctor: "apx profile doctor [<id>]",
|
|
27
|
+
uninstall: "apx profile uninstall <id>",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function fail(sub, msg) {
|
|
31
|
+
console.error(`apx profile ${sub}: ${msg}`);
|
|
32
|
+
console.error(`Usage: ${PROFILE_USAGE[sub]}`);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function asArray(v) {
|
|
37
|
+
if (v === undefined || v === null) return [];
|
|
38
|
+
return Array.isArray(v) ? v : [v];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** `--set k=v --set a=b` → { k: "v", a: "b" } */
|
|
42
|
+
function parseSetFlags(flags) {
|
|
43
|
+
const out = {};
|
|
44
|
+
for (const raw of asArray(flags?.set)) {
|
|
45
|
+
const s = String(raw);
|
|
46
|
+
const eq = s.indexOf("=");
|
|
47
|
+
if (eq < 1) {
|
|
48
|
+
console.error(`apx profile config: --set expects key=value — got "${s}"`);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
out[s.slice(0, eq).trim()] = s.slice(eq + 1);
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function printWarnings(warnings = []) {
|
|
57
|
+
for (const w of warnings) console.log(` warning: ${w}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function printDoctor(report) {
|
|
61
|
+
console.log(report.summary);
|
|
62
|
+
if (report.tokens != null) {
|
|
63
|
+
const budget = report.budget ? ` (declared budget ${report.budget})` : "";
|
|
64
|
+
console.log(` prompt: ~${report.tokens} tokens${budget}`);
|
|
65
|
+
}
|
|
66
|
+
for (const c of report.checks || []) {
|
|
67
|
+
const mark = c.level === "error" ? "✖" : "!";
|
|
68
|
+
console.log(` ${mark} [${c.label}] ${c.detail}`);
|
|
69
|
+
if (c.fix) console.log(` fix: ${c.fix}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ── list ────────────────────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
export async function cmdProfileList() {
|
|
76
|
+
const { active, profiles } = await http.get("/profiles");
|
|
77
|
+
if (!profiles.length) {
|
|
78
|
+
console.log("(no profiles available)");
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
for (const p of profiles) {
|
|
82
|
+
const mark = p.active ? "*" : " ";
|
|
83
|
+
const version = p.version ? ` v${p.version}` : "";
|
|
84
|
+
console.log(`${mark} ${p.id.padEnd(16)} ${p.name}${version} [${p.source}]`);
|
|
85
|
+
if (p.description) console.log(` ${p.description}`);
|
|
86
|
+
}
|
|
87
|
+
console.log("");
|
|
88
|
+
console.log(active ? `active: ${active}` : "active: none (vanilla)");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── show ────────────────────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
export async function cmdProfileShow(args) {
|
|
94
|
+
const id = args._[0];
|
|
95
|
+
if (!id) fail("show", "missing <id>");
|
|
96
|
+
|
|
97
|
+
const p = await http.get(`/profiles/${encodeURIComponent(id)}`);
|
|
98
|
+
console.log(`${p.name} (${p.id}) v${p.version || "?"} — ${p.source}${p.active ? " — ACTIVE" : ""}`);
|
|
99
|
+
if (p.description) console.log(p.description);
|
|
100
|
+
console.log(`languages: ${p.languages.join(", ") || "en"}`);
|
|
101
|
+
console.log(`prompt: ~${p.tokens} tokens${p.budget ? ` (declared budget ${p.budget})` : ""}`);
|
|
102
|
+
console.log(`path: ${p.dir}`);
|
|
103
|
+
|
|
104
|
+
const keys = Object.keys(p.config || {});
|
|
105
|
+
if (keys.length) {
|
|
106
|
+
console.log("\nsettings:");
|
|
107
|
+
for (const k of keys.sort()) console.log(` ${k.padEnd(24)} ${p.config[k]}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (args?.flags?.preview) {
|
|
111
|
+
console.log("\n--- rendered prompt block ---");
|
|
112
|
+
console.log(p.preview || "(empty)");
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ── install ─────────────────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
export async function cmdProfileInstall(args) {
|
|
119
|
+
const source = args._[0];
|
|
120
|
+
if (!source) fail("install", "missing <id|path>");
|
|
121
|
+
|
|
122
|
+
const r = await http.post("/profiles/install", { source, force: !!args?.flags?.force });
|
|
123
|
+
console.log(`installed ${r.profile.name} (${r.profile.id}) v${r.profile.version || "?"}`);
|
|
124
|
+
console.log(` prompt: ~${r.tokens} tokens`);
|
|
125
|
+
printWarnings(r.warnings);
|
|
126
|
+
console.log("");
|
|
127
|
+
console.log(`Not active yet — run: apx profile use ${r.profile.id}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ── use / off ───────────────────────────────────────────────────────────────
|
|
131
|
+
|
|
132
|
+
export async function cmdProfileUse(args) {
|
|
133
|
+
const id = args._[0];
|
|
134
|
+
if (!id) fail("use", "missing <id>");
|
|
135
|
+
|
|
136
|
+
const r = await http.post("/profiles/use", { id, force: !!args?.flags?.force });
|
|
137
|
+
console.log(`active profile: ${r.profile.name} (${r.profile.id})`);
|
|
138
|
+
printWarnings(r.warnings);
|
|
139
|
+
|
|
140
|
+
const { installed = [], skipped = [] } = r.routines || {};
|
|
141
|
+
if (installed.length) console.log(` routines installed: ${installed.join(", ")}`);
|
|
142
|
+
for (const s of skipped) {
|
|
143
|
+
console.log(` routine "${s.name}" left alone (${s.reason.replace(/_/g, " ")})`);
|
|
144
|
+
}
|
|
145
|
+
if (!r.profile.active) console.log(" (warning: profile did not activate)");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export async function cmdProfileOff() {
|
|
149
|
+
const r = await http.post("/profiles/off", {});
|
|
150
|
+
if (!r.was) {
|
|
151
|
+
console.log("no profile was active — nothing to do");
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
console.log(`profile "${r.was}" is off — APX is back to vanilla`);
|
|
155
|
+
if (r.routines?.length) console.log(` routines disabled (not deleted): ${r.routines.join(", ")}`);
|
|
156
|
+
console.log(" settings, tasks and memory were kept — `apx profile use` restores them");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ── config ──────────────────────────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
async function interactiveConfig(profile) {
|
|
162
|
+
const props = profile.schema?.properties || {};
|
|
163
|
+
const keys = Object.keys(props);
|
|
164
|
+
if (!keys.length) {
|
|
165
|
+
console.log(`profile "${profile.id}" has no configurable settings`);
|
|
166
|
+
return {};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
170
|
+
const values = {};
|
|
171
|
+
try {
|
|
172
|
+
console.log(`Configuring ${profile.name}. Press enter to keep the current value.\n`);
|
|
173
|
+
for (const key of keys) {
|
|
174
|
+
const def = props[key];
|
|
175
|
+
const current = profile.config?.[key];
|
|
176
|
+
const hint = def.enum ? ` (${def.enum.join(" | ")})` : def.type ? ` (${def.type})` : "";
|
|
177
|
+
const label = def.title || key;
|
|
178
|
+
const answer = (await rl.question(`${label}${hint} [${current ?? ""}]: `)).trim();
|
|
179
|
+
if (answer !== "") values[key] = answer;
|
|
180
|
+
}
|
|
181
|
+
} finally {
|
|
182
|
+
rl.close();
|
|
183
|
+
}
|
|
184
|
+
return values;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export async function cmdProfileConfig(args) {
|
|
188
|
+
const { active } = await http.get("/profiles");
|
|
189
|
+
const id = args?.flags?.profile || active;
|
|
190
|
+
if (!id) {
|
|
191
|
+
console.error("apx profile config: no profile is active — run: apx profile use <id>");
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const profile = await http.get(`/profiles/${encodeURIComponent(id)}?preview=0`);
|
|
196
|
+
|
|
197
|
+
let values = parseSetFlags(args?.flags);
|
|
198
|
+
if (args?.flags?.interactive) {
|
|
199
|
+
values = { ...values, ...(await interactiveConfig(profile)) };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// No changes asked for → show the current settings.
|
|
203
|
+
if (!Object.keys(values).length) {
|
|
204
|
+
const props = profile.schema?.properties || {};
|
|
205
|
+
const keys = Object.keys(profile.config || {});
|
|
206
|
+
if (!keys.length) {
|
|
207
|
+
console.log(`profile "${id}" has no settings`);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
console.log(`settings for ${profile.name} (${id}):`);
|
|
211
|
+
for (const k of keys.sort()) {
|
|
212
|
+
const def = props[k] || {};
|
|
213
|
+
const allowed = def.enum ? ` [${def.enum.join(" | ")}]` : "";
|
|
214
|
+
console.log(` ${k.padEnd(24)} ${profile.config[k]}${allowed}`);
|
|
215
|
+
}
|
|
216
|
+
console.log("\nchange one with: apx profile config --set key=value");
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const r = await http.patch("/profiles/config", { values, id });
|
|
221
|
+
console.log(`updated: ${r.changed.join(", ")}`);
|
|
222
|
+
for (const k of r.changed) console.log(` ${k.padEnd(24)} ${r.config[k]}`);
|
|
223
|
+
|
|
224
|
+
const { installed = [], skipped = [] } = r.routines || {};
|
|
225
|
+
if (installed.length) console.log(` routines rescheduled: ${installed.join(", ")}`);
|
|
226
|
+
for (const s of skipped) {
|
|
227
|
+
console.log(` routine "${s.name}" left alone (${s.reason.replace(/_/g, " ")})`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// ── doctor / uninstall ──────────────────────────────────────────────────────
|
|
232
|
+
|
|
233
|
+
export async function cmdProfileDoctor(args) {
|
|
234
|
+
const id = args._[0];
|
|
235
|
+
const q = id ? `?id=${encodeURIComponent(id)}` : "";
|
|
236
|
+
printDoctor(await http.get(`/profiles/doctor${q}`));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export async function cmdProfileUninstall(args) {
|
|
240
|
+
const id = args._[0];
|
|
241
|
+
if (!id) fail("uninstall", "missing <id>");
|
|
242
|
+
|
|
243
|
+
const r = await http.delete(`/profiles/${encodeURIComponent(id)}`);
|
|
244
|
+
console.log(`uninstalled "${r.id}" (${r.source})`);
|
|
245
|
+
if (r.routines?.removed?.length) console.log(` routines removed: ${r.routines.removed.join(", ")}`);
|
|
246
|
+
if (r.routines?.kept?.length) {
|
|
247
|
+
console.log(` kept (you edited these): ${r.routines.kept.join(", ")}`);
|
|
248
|
+
}
|
|
249
|
+
if (r.source === "bundled") {
|
|
250
|
+
console.log(" bundled package hidden — reinstall it any time with: apx profile install " + r.id);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// apx task — per-project TODO list. Backed by /projects/:pid/tasks.
|
|
2
2
|
//
|
|
3
3
|
// apx task add "<title>" [--project X] [--body Y] [--tag t] [--due 2026-05-30] [--agent A]
|
|
4
|
-
// apx task list [--project X] [--state
|
|
4
|
+
// apx task list [--all | --project X] [--state ...] [--status ...] [--tag X] [--agent Y]
|
|
5
|
+
// [--due-before ISO] [--due-after ISO] [--updated-since ISO] [--limit N]
|
|
5
6
|
// apx task show <id> [--project X]
|
|
6
7
|
// apx task done <id> [--project X] [--by name]
|
|
7
8
|
// apx task drop <id> [--project X] [--by name]
|
|
@@ -18,7 +19,7 @@ import { resolveProjectId } from "./project.js";
|
|
|
18
19
|
// ── Usage strings (also used by index.js help topics) ────────────────────────
|
|
19
20
|
export const TASK_USAGE = {
|
|
20
21
|
add: 'apx task add "<title>" [--project X] [--body Y] [--tag t]... [--due 2026-05-30] [--agent A]',
|
|
21
|
-
list: "apx task list [--project X] [--state open|done|dropped|all] [--tag X] [--agent Y] [--due-before ISO] [--limit N]",
|
|
22
|
+
list: "apx task list [--all | --project X] [--state open|done|dropped|all] [--status pending|running|in_review|blocked] [--tag X] [--agent Y] [--due-before ISO] [--due-after ISO] [--updated-since ISO] [--limit N]",
|
|
22
23
|
show: "apx task show <id> [--project X]",
|
|
23
24
|
done: "apx task done <id> [--project X] [--by name]",
|
|
24
25
|
drop: "apx task drop <id> [--project X] [--by name]",
|
|
@@ -44,14 +45,20 @@ function shortTs(iso) {
|
|
|
44
45
|
return String(iso).replace(/T/, " ").replace(/Z$/, "").slice(0, 16);
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
function renderTable(rows) {
|
|
48
|
+
function renderTable(rows, { showProject = false } = {}) {
|
|
48
49
|
if (!rows.length) {
|
|
49
50
|
console.log("(no tasks)");
|
|
50
51
|
return;
|
|
51
52
|
}
|
|
52
|
-
const idW = Math.max(...rows.map((r) => r.id.length), 4);
|
|
53
|
+
const idW = Math.max(...rows.map((r) => String(r.id).length), 4);
|
|
54
|
+
const projW = showProject
|
|
55
|
+
? Math.min(Math.max(...rows.map((r) => String(r.project_name || "").length), 7), 20)
|
|
56
|
+
: 0;
|
|
57
|
+
const proj = (t) => (showProject ? String(t.project_name || "").slice(0, projW).padEnd(projW) + " " : "");
|
|
58
|
+
|
|
53
59
|
console.log(
|
|
54
60
|
"ID".padEnd(idW) + " " +
|
|
61
|
+
(showProject ? "PROJECT".padEnd(projW) + " " : "") +
|
|
55
62
|
"STATE".padEnd(7) + " " +
|
|
56
63
|
"DUE".padEnd(10) + " " +
|
|
57
64
|
"TAGS".padEnd(18) + " " +
|
|
@@ -61,7 +68,8 @@ function renderTable(rows) {
|
|
|
61
68
|
const tags = (t.tags || []).join(",").slice(0, 18).padEnd(18);
|
|
62
69
|
const title = (t.title || "").slice(0, 60);
|
|
63
70
|
console.log(
|
|
64
|
-
t.id.padEnd(idW) + " " +
|
|
71
|
+
String(t.id).padEnd(idW) + " " +
|
|
72
|
+
proj(t) +
|
|
65
73
|
(t.state || "open").padEnd(7) + " " +
|
|
66
74
|
(t.due || "—").padEnd(10) + " " +
|
|
67
75
|
tags + " " +
|
|
@@ -105,17 +113,40 @@ export async function cmdTaskAdd(args) {
|
|
|
105
113
|
}
|
|
106
114
|
|
|
107
115
|
// ── list ──────────────────────────────────────────────────────────────────────
|
|
116
|
+
// The list endpoints answer with a { meta, data } envelope. Older callers here
|
|
117
|
+
// treated the response as a bare array, which made `apx task list` print
|
|
118
|
+
// "(no tasks)" no matter what — the rows were sitting in `.data`.
|
|
119
|
+
function unwrap(res) {
|
|
120
|
+
if (Array.isArray(res)) return { rows: res, meta: null };
|
|
121
|
+
return { rows: Array.isArray(res?.data) ? res.data : [], meta: res?.meta || null };
|
|
122
|
+
}
|
|
123
|
+
|
|
108
124
|
export async function cmdTaskList(args) {
|
|
109
|
-
const pid = await resolveProjectId(args?.flags?.project);
|
|
110
125
|
const params = new URLSearchParams();
|
|
111
|
-
if (args.flags?.state)
|
|
112
|
-
if (args.flags?.tag)
|
|
113
|
-
if (args.flags?.agent)
|
|
114
|
-
if (args.flags?.
|
|
115
|
-
if (args.flags?.
|
|
126
|
+
if (args.flags?.state) params.set("state", args.flags.state);
|
|
127
|
+
if (args.flags?.tag) params.set("tag", args.flags.tag);
|
|
128
|
+
if (args.flags?.agent) params.set("agent", args.flags.agent);
|
|
129
|
+
if (args.flags?.status) params.set("status", args.flags.status);
|
|
130
|
+
if (args.flags?.["due-before"]) params.set("due_before", args.flags["due-before"]);
|
|
131
|
+
if (args.flags?.["due-after"]) params.set("due_after", args.flags["due-after"]);
|
|
132
|
+
if (args.flags?.["updated-since"]) params.set("updated_since", args.flags["updated-since"]);
|
|
133
|
+
if (args.flags?.limit) params.set("limit", String(args.flags.limit));
|
|
116
134
|
const qs = params.toString();
|
|
117
|
-
|
|
118
|
-
|
|
135
|
+
|
|
136
|
+
// --all folds every registered project into one list, each row carrying the
|
|
137
|
+
// project it came from. Without it, behaviour is exactly as before.
|
|
138
|
+
const all = !!args.flags?.all;
|
|
139
|
+
const path = all
|
|
140
|
+
? `/tasks${qs ? "?" + qs : ""}`
|
|
141
|
+
: `/projects/${await resolveProjectId(args?.flags?.project)}/tasks${qs ? "?" + qs : ""}`;
|
|
142
|
+
|
|
143
|
+
const { rows, meta } = unwrap(await http.get(path));
|
|
144
|
+
renderTable(rows, { showProject: all });
|
|
145
|
+
|
|
146
|
+
// A project whose task log could not be read is reported, never swallowed.
|
|
147
|
+
for (const s of meta?.skipped || []) {
|
|
148
|
+
console.error(`warning: project #${s.id} skipped — ${s.error}`);
|
|
149
|
+
}
|
|
119
150
|
}
|
|
120
151
|
|
|
121
152
|
// ── show ──────────────────────────────────────────────────────────────────────
|