@agentprojectcontext/apx 1.77.3 → 1.79.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/README.md +23 -9
- package/package.json +12 -12
- package/src/core/agent/build-agent-system.js +3 -2
- package/src/core/agent/constants.js +3 -2
- package/src/core/agent/loop/greeting-guard.js +55 -0
- package/src/core/agent/loop/side-effects.js +52 -0
- package/src/core/agent/prompt-builder.js +49 -8
- package/src/core/agent/run-agent.js +43 -53
- package/src/core/agent/security.js +1 -1
- package/src/core/agent/self-memory.js +4 -2
- package/src/core/agent/skills/index-store.js +2 -2
- package/src/core/agent/skills/loader.js +3 -22
- package/src/core/agent/super-agent.js +0 -2
- package/src/core/agent/tool-summary.js +65 -0
- package/src/core/agent/tools/handlers/call-runtime.js +4 -4
- package/src/core/agent/tools/handlers/list-commitments.js +80 -0
- package/src/core/agent/tools/handlers/list-tasks.js +66 -27
- package/src/core/agent/tools/handlers/record-commitment.js +68 -0
- package/src/core/agent/tools/handlers/search-sessions.js +1 -1
- package/src/core/agent/tools/handlers/send-telegram.js +68 -2
- package/src/core/agent/tools/handlers/set-permission-mode.js +11 -4
- package/src/core/agent/tools/names.js +64 -0
- package/src/core/agent/tools/registry.js +9 -0
- package/src/core/agent/tools/tool-call-parser.js +70 -1
- package/src/core/apc/context-copy.js +1 -1
- package/src/core/apc/frontmatter.js +85 -0
- package/src/core/apc/parser.js +9 -21
- package/src/core/apc/paths.js +8 -8
- package/src/core/apc/scaffold.js +5 -4
- package/src/core/channels/telegram/ask-callbacks.js +35 -0
- package/src/core/channels/telegram/config.js +201 -0
- package/src/core/channels/telegram/dispatch.js +3 -0
- package/src/core/channels/telegram/reply.js +30 -5
- package/src/core/config/index.js +25 -183
- package/src/core/config/paths.js +57 -1
- package/src/core/config/redact.js +22 -0
- package/src/core/confirmation/adapters/web.js +1 -1
- package/src/core/daemon/service.js +238 -0
- package/src/core/deck/manifest.js +12 -12
- package/src/core/desktop/autostart.js +3 -2
- package/src/core/desktop/process.js +4 -4
- package/src/core/engines/gemini.js +322 -60
- package/src/core/engines/openai-compatible.js +21 -2
- package/src/core/engines/presets.js +2 -2
- package/src/core/http-tools/browser.js +1 -1
- package/src/core/http-tools/catalog.js +551 -0
- package/src/core/http-tools/fetch.js +1 -1
- package/src/core/http-tools/glob.js +3 -3
- package/src/core/http-tools/grep.js +2 -9
- package/src/core/http-tools/inline-handlers.js +59 -0
- package/src/core/http-tools/registry.js +31 -615
- package/src/core/http-tools/search.js +2 -2
- package/src/core/identity/self.js +6 -7
- package/src/core/identity/telegram.js +3 -3
- package/src/core/logging.js +6 -5
- package/src/core/mcp/sources.js +12 -16
- package/src/core/memory/consolidate.js +225 -0
- package/src/core/memory/indexer.js +2 -5
- package/src/core/nudge/index.js +192 -0
- package/src/core/nudge/policy.js +143 -0
- package/src/core/nudge/store.js +141 -0
- package/src/core/profiles/bundled/secretary/PROFILE.md +8 -9
- package/src/core/profiles/bundled/secretary/config.schema.json +33 -3
- package/src/core/profiles/bundled/secretary/routines/day-close.json +7 -3
- package/src/core/profiles/bundled/secretary/routines/day-open.json +7 -3
- package/src/core/profiles/bundled/secretary/routines/watch.json +13 -0
- package/src/core/profiles/lifecycle.js +0 -2
- package/src/core/profiles/store.js +2 -7
- package/src/core/routines/runner.js +103 -4
- package/src/core/routines/signals.js +270 -0
- package/src/core/runtime-skills/apx-agency-agents/SKILL.md +2 -2
- package/src/core/runtime-skills/apx-profile/SKILL.md +3 -3
- package/src/core/runtime-skills/apx-project/SKILL.md +1 -1
- package/src/core/runtime-skills/apx-task/SKILL.md +8 -8
- package/src/core/runtime-skills/apx-telegram/SKILL.md +4 -4
- package/src/core/runtime-skills/apx-voice/SKILL.md +4 -4
- package/src/core/runtimes/detect.js +2 -2
- package/src/core/sessions/index.js +800 -0
- package/src/core/stores/code-sessions.js +3 -14
- package/src/core/stores/commitments.js +331 -0
- package/src/core/stores/engine-sessions.js +0 -4
- package/src/core/stores/messages.js +4 -5
- package/src/core/stores/organization.js +9 -11
- package/src/core/stores/routines.js +23 -14
- package/src/core/stores/runtime-callbacks.js +2 -1
- package/src/core/util/json-file.js +128 -0
- package/src/core/util/thinking.js +51 -0
- package/src/core/vars/sources.js +14 -21
- package/src/core/voice/stt-models.js +1 -1
- package/src/core/voice/tts.js +5 -4
- package/src/host/daemon/api/admin-config.js +5 -5
- package/src/host/daemon/api/admin.js +8 -7
- package/src/host/daemon/api/agents.js +16 -16
- package/src/host/daemon/api/artifact-preview.js +8 -8
- package/src/host/daemon/api/artifacts.js +7 -7
- package/src/host/daemon/api/code.js +8 -8
- package/src/host/daemon/api/commitments.js +135 -0
- package/src/host/daemon/api/config.js +6 -6
- package/src/host/daemon/api/confirm.js +2 -2
- package/src/host/daemon/api/connections.js +2 -2
- package/src/host/daemon/api/conversations.js +12 -12
- package/src/host/daemon/api/deck.js +6 -6
- package/src/host/daemon/api/desktop.js +8 -8
- package/src/host/daemon/api/embeddings.js +4 -4
- package/src/host/daemon/api/engines.js +8 -7
- package/src/host/daemon/api/exec.js +5 -4
- package/src/host/daemon/api/files-project.js +6 -6
- package/src/host/daemon/api/health.js +2 -2
- package/src/host/daemon/api/identity.js +3 -3
- package/src/host/daemon/api/inbox.js +2 -2
- package/src/host/daemon/api/integrations.js +23 -19
- package/src/host/daemon/api/mcps.js +16 -12
- package/src/host/daemon/api/messages.js +5 -5
- package/src/host/daemon/api/nudges.js +112 -0
- package/src/host/daemon/api/organization.js +8 -8
- package/src/host/daemon/api/pairing.js +6 -6
- package/src/host/daemon/api/plugins.js +3 -3
- package/src/host/daemon/api/prefix.js +20 -0
- package/src/host/daemon/api/profiles.js +9 -9
- package/src/host/daemon/api/projects.js +5 -5
- package/src/host/daemon/api/routines.js +32 -8
- package/src/host/daemon/api/run.js +2 -2
- package/src/host/daemon/api/runtimes.js +6 -6
- package/src/host/daemon/api/self-memory.js +50 -0
- package/src/host/daemon/api/sessions-search.js +18 -11
- package/src/host/daemon/api/sessions.js +6 -6
- package/src/host/daemon/api/shared.js +59 -13
- package/src/host/daemon/api/skills.js +12 -12
- package/src/host/daemon/api/super-agent.js +4 -4
- package/src/host/daemon/api/tasks.js +11 -11
- package/src/host/daemon/api/telegram.js +61 -24
- package/src/host/daemon/api/tools.js +7 -7
- package/src/host/daemon/api/top-level.js +7 -7
- package/src/host/daemon/api/transcribe.js +6 -6
- package/src/host/daemon/api/tts.js +3 -3
- package/src/host/daemon/api/vars.js +11 -8
- package/src/host/daemon/api/voice.js +9 -7
- package/src/host/daemon/api/web.js +28 -32
- package/src/host/daemon/api.js +83 -48
- package/src/host/daemon/callback-reconciler.js +16 -0
- package/src/host/daemon/db.js +1 -1
- package/src/host/daemon/desktop-ws.js +7 -3
- package/src/host/daemon/index.js +11 -5
- package/src/host/daemon/plugins/desktop/index.js +8 -2
- package/src/host/daemon/plugins/telegram/index.js +7 -2
- package/src/host/daemon/stt-venv.js +20 -81
- package/src/host/daemon/wakeup.js +18 -4
- package/src/interfaces/acp/index.js +3 -3
- package/src/interfaces/acp/session.js +2 -2
- package/src/interfaces/cli/commands/a2a.js +2 -2
- package/src/interfaces/cli/commands/agent.js +3 -3
- package/src/interfaces/cli/commands/artifact.js +13 -13
- package/src/interfaces/cli/commands/chat.js +3 -3
- package/src/interfaces/cli/commands/command.js +2 -2
- package/src/interfaces/cli/commands/commitment.js +154 -0
- package/src/interfaces/cli/commands/config.js +4 -4
- package/src/interfaces/cli/commands/daemon.js +74 -18
- package/src/interfaces/cli/commands/desktop.js +7 -14
- package/src/interfaces/cli/commands/exec.js +2 -2
- package/src/interfaces/cli/commands/log.js +3 -4
- package/src/interfaces/cli/commands/mcp.js +9 -9
- package/src/interfaces/cli/commands/memory.js +75 -2
- package/src/interfaces/cli/commands/messages.js +5 -5
- package/src/interfaces/cli/commands/model.js +0 -18
- package/src/interfaces/cli/commands/nudge.js +130 -0
- package/src/interfaces/cli/commands/obsidian.js +5 -5
- package/src/interfaces/cli/commands/org.js +5 -5
- package/src/interfaces/cli/commands/pair.js +6 -6
- package/src/interfaces/cli/commands/plugins.js +2 -2
- package/src/interfaces/cli/commands/profile.js +10 -10
- package/src/interfaces/cli/commands/project-config.js +6 -6
- package/src/interfaces/cli/commands/project.js +10 -10
- package/src/interfaces/cli/commands/routine.js +8 -9
- package/src/interfaces/cli/commands/runtime.js +2 -2
- package/src/interfaces/cli/commands/search.js +0 -1
- package/src/interfaces/cli/commands/session.js +12 -40
- package/src/interfaces/cli/commands/sessions.js +21 -798
- package/src/interfaces/cli/commands/setup.js +16 -6
- package/src/interfaces/cli/commands/skills.js +1 -2
- package/src/interfaces/cli/commands/status.js +3 -3
- package/src/interfaces/cli/commands/task.js +8 -8
- package/src/interfaces/cli/commands/telegram.js +21 -21
- package/src/interfaces/cli/commands/voice.js +6 -6
- package/src/interfaces/cli/help/index.js +2245 -0
- package/src/interfaces/cli/http.js +9 -4
- package/src/interfaces/cli/index.js +14 -2857
- package/src/interfaces/cli/routes/acp.js +13 -0
- package/src/interfaces/cli/routes/agent.js +27 -0
- package/src/interfaces/cli/routes/artifact.js +28 -0
- package/src/interfaces/cli/routes/chat.js +11 -0
- package/src/interfaces/cli/routes/code.js +11 -0
- package/src/interfaces/cli/routes/command.js +21 -0
- package/src/interfaces/cli/routes/commitment.js +19 -0
- package/src/interfaces/cli/routes/config.js +16 -0
- package/src/interfaces/cli/routes/connections.js +11 -0
- package/src/interfaces/cli/routes/conversations.js +21 -0
- package/src/interfaces/cli/routes/daemon.js +25 -0
- package/src/interfaces/cli/routes/desktop.js +20 -0
- package/src/interfaces/cli/routes/env.js +13 -0
- package/src/interfaces/cli/routes/exec.js +11 -0
- package/src/interfaces/cli/routes/identity.js +11 -0
- package/src/interfaces/cli/routes/index.js +75 -0
- package/src/interfaces/cli/routes/init.js +11 -0
- package/src/interfaces/cli/routes/log.js +20 -0
- package/src/interfaces/cli/routes/mcp.js +22 -0
- package/src/interfaces/cli/routes/memory.js +19 -0
- package/src/interfaces/cli/routes/messages.js +16 -0
- package/src/interfaces/cli/routes/model.js +11 -0
- package/src/interfaces/cli/routes/nudge.js +17 -0
- package/src/interfaces/cli/routes/obsidian.js +17 -0
- package/src/interfaces/cli/routes/org.js +34 -0
- package/src/interfaces/cli/routes/overlay.js +10 -0
- package/src/interfaces/cli/routes/pair.js +17 -0
- package/src/interfaces/cli/routes/panel.js +16 -0
- package/src/interfaces/cli/routes/permission.js +11 -0
- package/src/interfaces/cli/routes/plugins.js +21 -0
- package/src/interfaces/cli/routes/profile.js +27 -0
- package/src/interfaces/cli/routes/project.js +41 -0
- package/src/interfaces/cli/routes/restart.js +15 -0
- package/src/interfaces/cli/routes/routine.js +28 -0
- package/src/interfaces/cli/routes/run.js +11 -0
- package/src/interfaces/cli/routes/search.js +11 -0
- package/src/interfaces/cli/routes/send.js +11 -0
- package/src/interfaces/cli/routes/session.js +26 -0
- package/src/interfaces/cli/routes/sessions.js +15 -0
- package/src/interfaces/cli/routes/setup.js +18 -0
- package/src/interfaces/cli/routes/skills.js +20 -0
- package/src/interfaces/cli/routes/status.js +12 -0
- package/src/interfaces/cli/routes/task.js +26 -0
- package/src/interfaces/cli/routes/telegram.js +39 -0
- package/src/interfaces/cli/routes/update.js +24 -0
- package/src/interfaces/cli/routes/voice.js +17 -0
- package/src/interfaces/desktop/main.js +6 -7
- package/src/interfaces/desktop/renderer.js +0 -5
- package/src/interfaces/mcp-server/index.js +15 -14
- package/src/interfaces/tui/_shims/globals.d.ts +8 -0
- package/src/interfaces/tui/tsconfig.json +4 -1
- package/src/interfaces/web/README.md +6 -6
- package/src/interfaces/web/dist/assets/index-CvEoGtTf.js +849 -0
- package/src/interfaces/web/dist/assets/index-CvEoGtTf.js.map +1 -0
- package/src/interfaces/web/dist/assets/index-DzBBXFaO.css +1 -0
- package/src/interfaces/web/dist/index.html +2 -2
- package/src/interfaces/web/package-lock.json +11 -10
- package/src/interfaces/web/src/components/AddProjectDialog.tsx +1 -1
- package/src/interfaces/web/src/components/Section.tsx +18 -3
- package/src/interfaces/web/src/components/agents/AgentFormFields.tsx +1 -1
- package/src/interfaces/web/src/components/chat/ChatList.tsx +2 -2
- package/src/interfaces/web/src/components/chat/MessageBubble.tsx +13 -0
- package/src/interfaces/web/src/components/chat/SkillPicker.tsx +1 -1
- package/src/interfaces/web/src/components/code/CodeFileTree.tsx +1 -1
- package/src/interfaces/web/src/components/code/CodeTerminal.tsx +1 -1
- package/src/interfaces/web/src/components/cron/CronPicker.tsx +196 -0
- package/src/interfaces/web/src/components/desktop/DesktopStatusCard.tsx +1 -1
- package/src/interfaces/web/src/components/files/FileBrowser.tsx +2 -2
- package/src/interfaces/web/src/components/inbox/InboxList.tsx +145 -0
- package/src/interfaces/web/src/components/memory/MemoryBrowser.tsx +36 -6
- package/src/interfaces/web/src/components/routines/ExecutionsList.tsx +1 -1
- package/src/interfaces/web/src/components/routines/RoutineDetail.tsx +16 -4
- package/src/interfaces/web/src/components/routines/RoutineEditor.tsx +14 -4
- package/src/interfaces/web/src/components/routines/shared.ts +14 -5
- package/src/interfaces/web/src/components/settings/DesktopSettingsPanel.tsx +1 -1
- package/src/interfaces/web/src/components/settings/MemoryPanel.tsx +1 -1
- package/src/interfaces/web/src/components/settings/NudgePanel.tsx +183 -0
- package/src/interfaces/web/src/components/settings/ProfilePanel.tsx +36 -12
- package/src/interfaces/web/src/components/settings/SkillsInspectorPanel.tsx +1 -1
- package/src/interfaces/web/src/components/settings/SkillsManager.tsx +2 -2
- package/src/interfaces/web/src/components/tasks/TaskDetailPanel.tsx +1 -1
- package/src/interfaces/web/src/components/ui/filter-chips.tsx +47 -0
- package/src/interfaces/web/src/components/ui.tsx +1 -0
- package/src/interfaces/web/src/constants/index.ts +1 -0
- package/src/interfaces/web/src/hooks/useChat.ts +5 -1
- package/src/interfaces/web/src/hooks/useDaemonStatus.ts +1 -1
- package/src/interfaces/web/src/hooks/useDevices.ts +1 -1
- package/src/interfaces/web/src/hooks/useEngines.ts +1 -1
- package/src/interfaces/web/src/hooks/useGlobalConfig.ts +2 -2
- package/src/interfaces/web/src/hooks/useIdentity.ts +1 -1
- package/src/interfaces/web/src/hooks/useInbox.ts +1 -1
- package/src/interfaces/web/src/hooks/useNudges.ts +38 -0
- package/src/interfaces/web/src/hooks/useProfiles.ts +3 -3
- package/src/interfaces/web/src/hooks/useProjects.ts +1 -1
- package/src/interfaces/web/src/hooks/useTelegram.ts +3 -3
- package/src/interfaces/web/src/hooks/useTokenBootstrap.ts +3 -3
- package/src/interfaces/web/src/i18n/en.ts +127 -0
- package/src/interfaces/web/src/i18n/es.ts +127 -0
- package/src/interfaces/web/src/lib/api/admin.ts +11 -11
- package/src/interfaces/web/src/lib/api/agents.ts +14 -14
- package/src/interfaces/web/src/lib/api/artifacts.ts +11 -11
- package/src/interfaces/web/src/lib/api/code.ts +1 -1
- package/src/interfaces/web/src/lib/api/commitments.ts +57 -0
- package/src/interfaces/web/src/lib/api/conversations.ts +8 -8
- package/src/interfaces/web/src/lib/api/deck.ts +3 -3
- package/src/interfaces/web/src/lib/api/desktop.ts +8 -8
- package/src/interfaces/web/src/lib/api/embeddings.ts +3 -3
- package/src/interfaces/web/src/lib/api/engines.ts +3 -3
- package/src/interfaces/web/src/lib/api/filesystem.ts +2 -2
- package/src/interfaces/web/src/lib/api/health.ts +1 -1
- package/src/interfaces/web/src/lib/api/identity.ts +2 -2
- package/src/interfaces/web/src/lib/api/inbox.ts +1 -1
- package/src/interfaces/web/src/lib/api/integrations.ts +8 -8
- package/src/interfaces/web/src/lib/api/mcps.ts +6 -6
- package/src/interfaces/web/src/lib/api/messages.ts +3 -3
- package/src/interfaces/web/src/lib/api/notebook.ts +23 -0
- package/src/interfaces/web/src/lib/api/nudges.ts +53 -0
- package/src/interfaces/web/src/lib/api/organization.ts +7 -7
- package/src/interfaces/web/src/lib/api/profiles.ts +8 -8
- package/src/interfaces/web/src/lib/api/projectFiles.ts +5 -5
- package/src/interfaces/web/src/lib/api/projects.ts +12 -12
- package/src/interfaces/web/src/lib/api/routines.ts +7 -7
- package/src/interfaces/web/src/lib/api/sessions.ts +2 -2
- package/src/interfaces/web/src/lib/api/skills.ts +11 -11
- package/src/interfaces/web/src/lib/api/super_agent.ts +3 -3
- package/src/interfaces/web/src/lib/api/tasks.ts +12 -12
- package/src/interfaces/web/src/lib/api/telegram.ts +14 -14
- package/src/interfaces/web/src/lib/api/tools.ts +1 -1
- package/src/interfaces/web/src/lib/api/vars.ts +4 -4
- package/src/interfaces/web/src/lib/api/voice.ts +6 -6
- package/src/interfaces/web/src/lib/cron.ts +196 -0
- package/src/interfaces/web/src/lib/when.ts +32 -0
- package/src/interfaces/web/src/screens/InboxScreen.tsx +107 -77
- package/src/interfaces/web/src/screens/ProjectScreen.tsx +5 -2
- package/src/interfaces/web/src/screens/SettingsScreen.tsx +17 -3
- package/src/interfaces/web/src/screens/base/AgentDefaultsTab.tsx +1 -1
- package/src/interfaces/web/src/screens/base/CommitmentsTab.tsx +239 -0
- package/src/interfaces/web/src/screens/base/GlobalTasksTab.tsx +103 -20
- package/src/interfaces/web/src/screens/base/LogsTab.tsx +18 -3
- package/src/interfaces/web/src/screens/base/SessionsTab.tsx +1 -1
- package/src/interfaces/web/src/screens/modules/CodeScreen.tsx +1 -1
- package/src/interfaces/web/src/screens/modules/DeckScreen.tsx +1 -1
- package/src/interfaces/web/src/screens/modules/DesktopScreen.tsx +1 -1
- package/src/interfaces/web/src/screens/modules/VoiceScreen.tsx +1 -1
- package/src/interfaces/web/src/screens/project/AgentDetailScreen.tsx +8 -8
- package/src/interfaces/web/src/screens/project/AgentsTab.tsx +2 -2
- package/src/interfaces/web/src/screens/project/ChatTab.tsx +24 -6
- package/src/interfaces/web/src/screens/project/ConfigTab.tsx +1 -1
- package/src/interfaces/web/src/screens/project/McpsTab.tsx +3 -3
- package/src/interfaces/web/src/screens/project/Overview.tsx +6 -6
- package/src/interfaces/web/src/screens/project/RoutinesTab.tsx +15 -13
- package/src/interfaces/web/src/screens/project/StructureTab.tsx +1 -1
- package/src/interfaces/web/src/screens/project/TasksTab.tsx +1 -1
- package/src/interfaces/web/src/screens/project/VarsTab.tsx +1 -1
- package/src/interfaces/web/src/types/daemon.ts +10 -1
- package/src/interfaces/web/vite.config.ts +13 -18
- package/src/interfaces/web/dist/assets/index-BptlbKjU.js +0 -824
- package/src/interfaces/web/dist/assets/index-BptlbKjU.js.map +0 -1
- package/src/interfaces/web/dist/assets/index-D_EJEA1n.css +0 -1
- package/src/skills/apc-context/SKILL.md +0 -159
- /package/src/{host/daemon → core}/runtimes/_spawn.js +0 -0
- /package/src/{host/daemon → core}/runtimes/aider.js +0 -0
- /package/src/{host/daemon → core}/runtimes/antigravity.js +0 -0
- /package/src/{host/daemon → core}/runtimes/claude-code.js +0 -0
- /package/src/{host/daemon → core}/runtimes/codex.js +0 -0
- /package/src/{host/daemon → core}/runtimes/cursor-agent.js +0 -0
- /package/src/{host/daemon → core}/runtimes/gemini-cli.js +0 -0
- /package/src/{host/daemon → core}/runtimes/index.js +0 -0
- /package/src/{host/daemon → core}/runtimes/opencode.js +0 -0
- /package/src/{host/daemon → core}/runtimes/qwen-code.js +0 -0
|
@@ -14,6 +14,7 @@ import path from "node:path";
|
|
|
14
14
|
import { nowIso } from "../util/time.js";
|
|
15
15
|
import { shortId as makeShortId } from "../util/ids.js";
|
|
16
16
|
import { CODE_MODES, DEFAULT_CODE_MODE } from "../constants/code-modes.js";
|
|
17
|
+
import { readJson, writeJson } from "#core/util/json-file.js";
|
|
17
18
|
|
|
18
19
|
function sessionsDir(storagePath) {
|
|
19
20
|
return path.join(storagePath, "code-sessions");
|
|
@@ -28,20 +29,8 @@ function shortId() {
|
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
// Atomic write: tmp file + rename so a crash mid-write can't corrupt a session.
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
const tmp = `${file}.${process.pid}.tmp`;
|
|
34
|
-
fs.writeFileSync(tmp, JSON.stringify(obj, null, 2));
|
|
35
|
-
fs.renameSync(tmp, file);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function readJson(file) {
|
|
39
|
-
try {
|
|
40
|
-
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
41
|
-
} catch {
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
32
|
+
// readJson/writeJson come from the shared kernel (this module's atomic write
|
|
33
|
+
// was the one the others were missing).
|
|
45
34
|
|
|
46
35
|
/** Lightweight row for the session list (no messages). */
|
|
47
36
|
function toRow(s) {
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
// Commitments per project.
|
|
2
|
+
//
|
|
3
|
+
// Append-only JSONL event log, one file per month under
|
|
4
|
+
// ~/.apx/projects/<apxId>/commitments/YYYY-MM.jsonl
|
|
5
|
+
//
|
|
6
|
+
// A TASK is something to do. A COMMITMENT is something PROMISED TO A PERSON:
|
|
7
|
+
// it has a counterparty, a date you gave them, and the channel you said it on.
|
|
8
|
+
// Breaking one costs trust, not just throughput, and that is why it is a type
|
|
9
|
+
// and not a tag.
|
|
10
|
+
//
|
|
11
|
+
// The tag version is tempting and wrong. The day you want "everything I owe
|
|
12
|
+
// Ana" you would be substring-matching your way through task titles, and
|
|
13
|
+
// "kept" versus "renegotiated" — the distinction that tells you whether a
|
|
14
|
+
// relationship is fine — has nowhere to live at all.
|
|
15
|
+
//
|
|
16
|
+
// Events:
|
|
17
|
+
// create — the promise (counterparty, body, due, origin_channel, …)
|
|
18
|
+
// update — shallow-merge patch
|
|
19
|
+
// kept — delivered
|
|
20
|
+
// missed — the date passed without delivery. Recorded, not hidden:
|
|
21
|
+
// a system that quietly drops what you failed to do cannot
|
|
22
|
+
// tell you that you keep failing the same person.
|
|
23
|
+
// renegotiate — a NEW date, agreed with them. Distinct from missing:
|
|
24
|
+
// moving a date with someone is not the same as letting it
|
|
25
|
+
// slide, and the history keeps both.
|
|
26
|
+
//
|
|
27
|
+
// State: "open" → "kept" | "missed" | "renegotiated" → (renegotiated reopens
|
|
28
|
+
// as "open" with a new due, keeping the previous date in `history`).
|
|
29
|
+
import fs from "node:fs";
|
|
30
|
+
import path from "node:path";
|
|
31
|
+
import { nowIso } from "../util/time.js";
|
|
32
|
+
import { shortId as makeShortId } from "../util/ids.js";
|
|
33
|
+
|
|
34
|
+
export const COMMITMENT_STATES = Object.freeze(["open", "kept", "missed", "renegotiated"]);
|
|
35
|
+
|
|
36
|
+
function commitmentsDir(storagePath) {
|
|
37
|
+
return path.join(storagePath, "commitments");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function monthlyFile(storagePath, date = new Date()) {
|
|
41
|
+
const ym = date.toISOString().slice(0, 7); // YYYY-MM
|
|
42
|
+
return path.join(commitmentsDir(storagePath), `${ym}.jsonl`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function shortId() {
|
|
46
|
+
return makeShortId("c");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function appendEvent(storagePath, event) {
|
|
50
|
+
const file = monthlyFile(storagePath);
|
|
51
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
52
|
+
fs.appendFileSync(file, JSON.stringify(event) + "\n");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function readAllEvents(storagePath) {
|
|
56
|
+
const dir = commitmentsDir(storagePath);
|
|
57
|
+
if (!fs.existsSync(dir)) return [];
|
|
58
|
+
const files = fs.readdirSync(dir).filter((f) => f.endsWith(".jsonl")).sort();
|
|
59
|
+
const events = [];
|
|
60
|
+
for (const f of files) {
|
|
61
|
+
const text = fs.readFileSync(path.join(dir, f), "utf8");
|
|
62
|
+
for (const line of text.split("\n")) {
|
|
63
|
+
if (!line.trim()) continue;
|
|
64
|
+
try {
|
|
65
|
+
const ev = JSON.parse(line);
|
|
66
|
+
if (ev && ev.id && ev.op) events.push(ev);
|
|
67
|
+
} catch {
|
|
68
|
+
// One bad write must not blank the projection.
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
events.sort((a, b) => (a.ts || "").localeCompare(b.ts || ""));
|
|
73
|
+
return events;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function projectState(events) {
|
|
77
|
+
const rows = new Map();
|
|
78
|
+
for (const ev of events) {
|
|
79
|
+
const existing = rows.get(ev.id);
|
|
80
|
+
switch (ev.op) {
|
|
81
|
+
case "create": {
|
|
82
|
+
if (existing) break; // duplicate create — keep the first
|
|
83
|
+
rows.set(ev.id, {
|
|
84
|
+
id: ev.id,
|
|
85
|
+
created_at: ev.ts,
|
|
86
|
+
updated_at: ev.ts,
|
|
87
|
+
state: "open",
|
|
88
|
+
counterparty: ev.counterparty || "",
|
|
89
|
+
body: ev.body || "",
|
|
90
|
+
promised_at: ev.promised_at || ev.ts,
|
|
91
|
+
due: ev.due || null,
|
|
92
|
+
origin_channel: ev.origin_channel || null,
|
|
93
|
+
origin_message_ref: ev.origin_message_ref || null,
|
|
94
|
+
created_by: ev.created_by || null,
|
|
95
|
+
// Every date this promise has ever had. Renegotiating twice is a
|
|
96
|
+
// fact about the relationship, and it is only visible if kept.
|
|
97
|
+
history: [],
|
|
98
|
+
meta: ev.meta && typeof ev.meta === "object" ? { ...ev.meta } : {},
|
|
99
|
+
});
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
case "update": {
|
|
103
|
+
if (!existing) break;
|
|
104
|
+
const patch = ev.patch && typeof ev.patch === "object" ? ev.patch : {};
|
|
105
|
+
for (const k of Object.keys(patch)) {
|
|
106
|
+
if (k === "id" || k === "state" || k === "created_at" || k === "history") continue;
|
|
107
|
+
existing[k] = patch[k];
|
|
108
|
+
}
|
|
109
|
+
existing.updated_at = ev.ts;
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
case "kept": {
|
|
113
|
+
if (!existing) break;
|
|
114
|
+
existing.state = "kept";
|
|
115
|
+
existing.closed_at = ev.ts;
|
|
116
|
+
existing.note = ev.note || existing.note || null;
|
|
117
|
+
existing.updated_at = ev.ts;
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
case "missed": {
|
|
121
|
+
if (!existing) break;
|
|
122
|
+
existing.state = "missed";
|
|
123
|
+
existing.closed_at = ev.ts;
|
|
124
|
+
existing.note = ev.note || existing.note || null;
|
|
125
|
+
existing.updated_at = ev.ts;
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
case "renegotiate": {
|
|
129
|
+
if (!existing) break;
|
|
130
|
+
existing.history.push({
|
|
131
|
+
due: existing.due,
|
|
132
|
+
moved_at: ev.ts,
|
|
133
|
+
note: ev.note || null,
|
|
134
|
+
});
|
|
135
|
+
existing.due = ev.due || existing.due;
|
|
136
|
+
// Back to open: a renegotiated promise is a live promise with a new
|
|
137
|
+
// date, not a closed one. "renegotiated" as a resting state would hide
|
|
138
|
+
// it from every "what do I owe people" view.
|
|
139
|
+
existing.state = "open";
|
|
140
|
+
existing.renegotiated_count = (existing.renegotiated_count || 0) + 1;
|
|
141
|
+
existing.updated_at = ev.ts;
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
default:
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return rows;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
152
|
+
// Public API
|
|
153
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Record a promise.
|
|
157
|
+
* fields: { counterparty (required), body (required), due?, promised_at?,
|
|
158
|
+
* origin_channel?, origin_message_ref?, created_by?, meta? }
|
|
159
|
+
*/
|
|
160
|
+
export function createCommitment(storagePath, fields) {
|
|
161
|
+
if (!fields || typeof fields !== "object") throw new Error("createCommitment: fields required");
|
|
162
|
+
if (!fields.counterparty || typeof fields.counterparty !== "string") {
|
|
163
|
+
// The counterparty IS the type. Without it this is a task.
|
|
164
|
+
throw new Error("createCommitment: counterparty required");
|
|
165
|
+
}
|
|
166
|
+
if (!fields.body || typeof fields.body !== "string") {
|
|
167
|
+
throw new Error("createCommitment: body required");
|
|
168
|
+
}
|
|
169
|
+
const id = shortId();
|
|
170
|
+
appendEvent(storagePath, {
|
|
171
|
+
id,
|
|
172
|
+
ts: nowIso(),
|
|
173
|
+
op: "create",
|
|
174
|
+
counterparty: fields.counterparty.trim(),
|
|
175
|
+
body: fields.body.trim(),
|
|
176
|
+
promised_at: fields.promised_at || nowIso(),
|
|
177
|
+
due: fields.due || null,
|
|
178
|
+
origin_channel: fields.origin_channel || null,
|
|
179
|
+
origin_message_ref: fields.origin_message_ref || null,
|
|
180
|
+
created_by: fields.created_by || null,
|
|
181
|
+
meta: fields.meta && typeof fields.meta === "object" ? fields.meta : {},
|
|
182
|
+
});
|
|
183
|
+
return getCommitment(storagePath, id);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** Newest first, id as tiebreak — same reasoning as tasks.js byNewest. */
|
|
187
|
+
function byNewest(a, b) {
|
|
188
|
+
const t = (b.created_at || "").localeCompare(a.created_at || "");
|
|
189
|
+
return t !== 0 ? t : String(b.id || "").localeCompare(String(a.id || ""));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** Soonest deadline first; undated last. The order the anchors want. */
|
|
193
|
+
function byDue(a, b) {
|
|
194
|
+
if (!a.due && !b.due) return byNewest(a, b);
|
|
195
|
+
if (!a.due) return 1;
|
|
196
|
+
if (!b.due) return -1;
|
|
197
|
+
const d = a.due.localeCompare(b.due);
|
|
198
|
+
return d !== 0 ? d : byNewest(a, b);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* List commitments.
|
|
203
|
+
*
|
|
204
|
+
* opts: { state, counterparty, due_before, due_after, overdue, updated_since,
|
|
205
|
+
* sort: "due"|"newest", limit }
|
|
206
|
+
* Default state is "open" — the useful question is what you still owe.
|
|
207
|
+
*/
|
|
208
|
+
export function listCommitments(storagePath, opts = {}) {
|
|
209
|
+
let out = [...projectState(readAllEvents(storagePath)).values()];
|
|
210
|
+
|
|
211
|
+
if (opts.state && opts.state !== "all") {
|
|
212
|
+
out = out.filter((c) => c.state === opts.state);
|
|
213
|
+
} else if (!opts.state) {
|
|
214
|
+
out = out.filter((c) => c.state === "open");
|
|
215
|
+
}
|
|
216
|
+
if (opts.counterparty) {
|
|
217
|
+
// Case-insensitive substring: counterparty is free text, not a CRM key,
|
|
218
|
+
// so "ana" must find "Ana Pérez" or the field is unusable.
|
|
219
|
+
const needle = String(opts.counterparty).toLowerCase();
|
|
220
|
+
out = out.filter((c) => String(c.counterparty).toLowerCase().includes(needle));
|
|
221
|
+
}
|
|
222
|
+
if (opts.due_before) out = out.filter((c) => c.due && c.due <= opts.due_before);
|
|
223
|
+
if (opts.due_after) out = out.filter((c) => c.due && c.due >= opts.due_after);
|
|
224
|
+
if (opts.overdue) {
|
|
225
|
+
const now = opts.now || nowIso();
|
|
226
|
+
out = out.filter((c) => c.state === "open" && c.due && c.due < now);
|
|
227
|
+
}
|
|
228
|
+
if (opts.updated_since) {
|
|
229
|
+
out = out.filter((c) => (c.updated_at || c.created_at || "") >= opts.updated_since);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
out.sort(opts.sort === "newest" ? byNewest : byDue);
|
|
233
|
+
if (opts.limit && Number.isFinite(opts.limit)) out = out.slice(0, opts.limit);
|
|
234
|
+
return out;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* The same query folded across registered projects. Mirrors
|
|
239
|
+
* listTasksAcrossProjects — a chief of staff lives in the cross-project layer,
|
|
240
|
+
* and a promise made in a meeting rarely knows which repo it belongs to.
|
|
241
|
+
*
|
|
242
|
+
* A project whose log is unreadable is SKIPPED, not fatal.
|
|
243
|
+
*/
|
|
244
|
+
export function listCommitmentsAcrossProjects(projects, opts = {}) {
|
|
245
|
+
const { limit, ...perProject } = opts || {};
|
|
246
|
+
const commitments = [];
|
|
247
|
+
const skipped = [];
|
|
248
|
+
|
|
249
|
+
for (const entry of projects || []) {
|
|
250
|
+
if (!entry?.storagePath) continue;
|
|
251
|
+
try {
|
|
252
|
+
for (const c of listCommitments(entry.storagePath, perProject)) {
|
|
253
|
+
commitments.push({
|
|
254
|
+
...c,
|
|
255
|
+
project_id: entry.id,
|
|
256
|
+
project_name: entry.name || entry.path || String(entry.id),
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
} catch (e) {
|
|
260
|
+
skipped.push({ id: entry.id, error: e?.message || String(e) });
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
commitments.sort(opts.sort === "newest" ? byNewest : byDue);
|
|
265
|
+
return {
|
|
266
|
+
commitments: Number.isFinite(limit) && limit > 0 ? commitments.slice(0, limit) : commitments,
|
|
267
|
+
skipped,
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Get one by id or unique id prefix (≥ 3 chars). */
|
|
272
|
+
export function getCommitment(storagePath, idOrPrefix) {
|
|
273
|
+
if (!idOrPrefix || typeof idOrPrefix !== "string") return null;
|
|
274
|
+
const rows = projectState(readAllEvents(storagePath));
|
|
275
|
+
if (rows.has(idOrPrefix)) return rows.get(idOrPrefix);
|
|
276
|
+
if (idOrPrefix.length < 3) return null;
|
|
277
|
+
const matches = [...rows.values()].filter((c) => c.id.startsWith(idOrPrefix));
|
|
278
|
+
return matches.length === 1 ? matches[0] : null;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function patchCommitment(storagePath, idOrPrefix, patch) {
|
|
282
|
+
const existing = getCommitment(storagePath, idOrPrefix);
|
|
283
|
+
if (!existing) return null;
|
|
284
|
+
if (!patch || typeof patch !== "object") return existing;
|
|
285
|
+
appendEvent(storagePath, { id: existing.id, ts: nowIso(), op: "update", patch });
|
|
286
|
+
return getCommitment(storagePath, existing.id);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Delivered. */
|
|
290
|
+
export function keepCommitment(storagePath, idOrPrefix, note = null) {
|
|
291
|
+
return close(storagePath, idOrPrefix, "kept", note);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** The date passed and it did not happen. */
|
|
295
|
+
export function missCommitment(storagePath, idOrPrefix, note = null) {
|
|
296
|
+
return close(storagePath, idOrPrefix, "missed", note);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function close(storagePath, idOrPrefix, op, note) {
|
|
300
|
+
const existing = getCommitment(storagePath, idOrPrefix);
|
|
301
|
+
if (!existing) return null;
|
|
302
|
+
appendEvent(storagePath, { id: existing.id, ts: nowIso(), op, note: note || null });
|
|
303
|
+
return getCommitment(storagePath, existing.id);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* A new date, agreed with them. Requires the new date: "renegotiated, no idea
|
|
308
|
+
* until when" is how a promise disappears.
|
|
309
|
+
*/
|
|
310
|
+
export function renegotiateCommitment(storagePath, idOrPrefix, due, note = null) {
|
|
311
|
+
if (!due) throw new Error("renegotiateCommitment: a new due date is required");
|
|
312
|
+
const existing = getCommitment(storagePath, idOrPrefix);
|
|
313
|
+
if (!existing) return null;
|
|
314
|
+
appendEvent(storagePath, {
|
|
315
|
+
id: existing.id, ts: nowIso(), op: "renegotiate", due, note: note || null,
|
|
316
|
+
});
|
|
317
|
+
return getCommitment(storagePath, existing.id);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** Counts for status displays and anchors. */
|
|
321
|
+
export function countCommitments(storagePath, now = nowIso()) {
|
|
322
|
+
const rows = [...projectState(readAllEvents(storagePath)).values()];
|
|
323
|
+
const open = rows.filter((c) => c.state === "open");
|
|
324
|
+
return {
|
|
325
|
+
open: open.length,
|
|
326
|
+
kept: rows.filter((c) => c.state === "kept").length,
|
|
327
|
+
missed: rows.filter((c) => c.state === "missed").length,
|
|
328
|
+
overdue: open.filter((c) => c.due && c.due < now).length,
|
|
329
|
+
total: rows.length,
|
|
330
|
+
};
|
|
331
|
+
}
|
|
@@ -13,10 +13,6 @@ import path from "node:path";
|
|
|
13
13
|
|
|
14
14
|
const homeDir = (opts) => (opts && opts.home) || os.homedir();
|
|
15
15
|
|
|
16
|
-
function encodeClaudeProjectPath(cwd) {
|
|
17
|
-
return String(cwd || "").replace(/[^A-Za-z0-9]/g, "-");
|
|
18
|
-
}
|
|
19
|
-
|
|
20
16
|
function safeStatMtime(p) {
|
|
21
17
|
try {
|
|
22
18
|
return fs.statSync(p).mtimeMs;
|
|
@@ -29,11 +29,6 @@ function dayPathJsonl(projectRoot, ts) {
|
|
|
29
29
|
return path.join(projectRoot, "messages", `${day}.jsonl`);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
function dayPathMd(projectRoot, ts) {
|
|
33
|
-
const day = (ts || nowIso()).slice(0, 10);
|
|
34
|
-
return path.join(projectRoot, "messages", `${day}.md`);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
32
|
// `compact` is a progressive-compaction summary record (Pieza 3): a dense
|
|
38
33
|
// recap of older turns, stored inline in the channel JSONL so the reader can
|
|
39
34
|
// prepend it as a [RESUMEN COMPACTADO] system turn instead of replaying the
|
|
@@ -689,6 +684,10 @@ export function readGlobalThread({ channel, date, _globalMessagesDir } = {}) {
|
|
|
689
684
|
...(r.actor_kind ? { actor_kind: r.actor_kind } : {}),
|
|
690
685
|
...(r.meta?.model ? { model: r.meta.model } : {}),
|
|
691
686
|
...(usage && typeof usage === "object" ? { usage } : {}),
|
|
687
|
+
// What the turn actually did. Recorded compactly at write time
|
|
688
|
+
// (core/agent/tool-summary.js) because the live tool events are gone
|
|
689
|
+
// by the time anyone reads the thread back.
|
|
690
|
+
...(r.meta?.tool_summary ? { tool_summary: r.meta.tool_summary } : {}),
|
|
692
691
|
};
|
|
693
692
|
});
|
|
694
693
|
return { id: date, channel, messages };
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import fs from "node:fs";
|
|
20
20
|
import path from "node:path";
|
|
21
21
|
import { apcOrganizationFile } from "../apc/paths.js";
|
|
22
|
+
import { readJson } from "#core/util/json-file.js";
|
|
22
23
|
|
|
23
24
|
export const ORG_SLUG_RE = /^[a-z][a-z0-9_-]*$/;
|
|
24
25
|
|
|
@@ -39,17 +40,14 @@ function emptyOrg() {
|
|
|
39
40
|
|
|
40
41
|
export function readOrganization(root) {
|
|
41
42
|
const file = apcOrganizationFile(root);
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
}
|
|
50
|
-
// A corrupt file shouldn't take down the whole project view.
|
|
51
|
-
return emptyOrg();
|
|
52
|
-
}
|
|
43
|
+
// A missing or corrupt file shouldn't take down the whole project view —
|
|
44
|
+
// readJson returns the fallback for both.
|
|
45
|
+
const raw = readJson(file, null);
|
|
46
|
+
if (!raw) return emptyOrg();
|
|
47
|
+
return {
|
|
48
|
+
areas: Array.isArray(raw.areas) ? raw.areas : [],
|
|
49
|
+
roles: Array.isArray(raw.roles) ? raw.roles : [],
|
|
50
|
+
};
|
|
53
51
|
}
|
|
54
52
|
|
|
55
53
|
function writeOrganization(root, org) {
|
|
@@ -5,6 +5,7 @@ import path from "node:path";
|
|
|
5
5
|
import { CronExpressionParser } from "cron-parser";
|
|
6
6
|
import { nowIso, isoToMs } from "../util/time.js";
|
|
7
7
|
import { shortId } from "../util/ids.js";
|
|
8
|
+
import { readJson, writeJson } from "#core/util/json-file.js";
|
|
8
9
|
|
|
9
10
|
function routinesPath(storagePath) {
|
|
10
11
|
// storagePath is always ~/.apx/projects/{apxId}/ — flat, no .apc subdir needed.
|
|
@@ -12,26 +13,31 @@ function routinesPath(storagePath) {
|
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
function readFile(projectPath) {
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
try {
|
|
18
|
-
const raw = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
19
|
-
return Array.isArray(raw.routines) ? raw.routines : [];
|
|
20
|
-
} catch {
|
|
21
|
-
return [];
|
|
22
|
-
}
|
|
16
|
+
const raw = readJson(routinesPath(projectPath), {});
|
|
17
|
+
return Array.isArray(raw?.routines) ? raw.routines : [];
|
|
23
18
|
}
|
|
24
19
|
|
|
20
|
+
// Atomic: a crash mid-write used to leave a truncated routines.json, and since
|
|
21
|
+
// the reader swallows parse errors the next read reported "no routines".
|
|
25
22
|
function writeFile(projectPath, routines) {
|
|
26
|
-
|
|
27
|
-
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
28
|
-
fs.writeFileSync(p, JSON.stringify({ routines }, null, 2) + "\n");
|
|
23
|
+
writeJson(routinesPath(projectPath), { routines });
|
|
29
24
|
}
|
|
30
25
|
|
|
31
26
|
// --------------------- schedule parsing -------------------------------------
|
|
32
27
|
|
|
33
28
|
export function parseSchedule(s, baseMs = Date.now()) {
|
|
34
29
|
if (!s || typeof s !== "string") return { kind: "invalid" };
|
|
30
|
+
|
|
31
|
+
// "manual" — runs only when someone runs it. It USED to work by accident:
|
|
32
|
+
// the string failed cron parsing, came back "invalid", and an invalid
|
|
33
|
+
// schedule never becomes due. Correct outcome, wrong reason, and it meant
|
|
34
|
+
// every surface reported a deliberate choice as a broken expression.
|
|
35
|
+
if (s.trim().toLowerCase() === "manual") return { kind: "manual" };
|
|
36
|
+
|
|
37
|
+
// Tolerate a leading "cron " label. The web editor's own presets wrote
|
|
38
|
+
// `cron 0 9 * * *`, which cron-parser rejects — so picking "daily at 9am"
|
|
39
|
+
// in the panel produced a routine that never ran, with nothing to show why.
|
|
40
|
+
const labelled = s.trim().replace(/^cron\s+/i, "");
|
|
35
41
|
|
|
36
42
|
if (s.startsWith("every:")) {
|
|
37
43
|
const spec = s.slice(6).trim();
|
|
@@ -51,7 +57,7 @@ export function parseSchedule(s, baseMs = Date.now()) {
|
|
|
51
57
|
|
|
52
58
|
// Fallback: Try parsing as standard cron expression using cron-parser
|
|
53
59
|
try {
|
|
54
|
-
const interval = CronExpressionParser.parse(
|
|
60
|
+
const interval = CronExpressionParser.parse(labelled, { currentDate: new Date(baseMs) });
|
|
55
61
|
return { kind: "cron", parser: interval };
|
|
56
62
|
} catch (err) {
|
|
57
63
|
return { kind: "invalid" };
|
|
@@ -60,7 +66,7 @@ export function parseSchedule(s, baseMs = Date.now()) {
|
|
|
60
66
|
|
|
61
67
|
export function computeNextRun(routine, baseMs = Date.now()) {
|
|
62
68
|
const sched = parseSchedule(routine.schedule, baseMs);
|
|
63
|
-
if (sched.kind === "invalid") return null;
|
|
69
|
+
if (sched.kind === "invalid" || sched.kind === "manual") return null;
|
|
64
70
|
if (sched.kind === "once") {
|
|
65
71
|
return sched.atMs > baseMs
|
|
66
72
|
? new Date(sched.atMs).toISOString().replace(/\.\d{3}Z$/, "Z")
|
|
@@ -232,7 +238,10 @@ export function getDueRoutines(projectPath, nowStr) {
|
|
|
232
238
|
// CRITICAL: If the schedule cannot be parsed, NEVER run it.
|
|
233
239
|
// Otherwise, an invalid schedule (like a cron string) sets next_run_at to null,
|
|
234
240
|
// which previously caused it to be considered ALWAYS due and spam execution every 5 seconds!
|
|
235
|
-
|
|
241
|
+
const kind = parseSchedule(r.schedule).kind;
|
|
242
|
+
// "manual" is a deliberate never-on-a-clock, not a broken expression —
|
|
243
|
+
// both are skipped here, but only one of them is a problem to report.
|
|
244
|
+
if (kind === "invalid" || kind === "manual") return false;
|
|
236
245
|
return (!r.next_run_at || r.next_run_at <= nowStr);
|
|
237
246
|
});
|
|
238
247
|
}
|
|
@@ -18,6 +18,7 @@ import fs from "node:fs";
|
|
|
18
18
|
import path from "node:path";
|
|
19
19
|
import { APX_HOME } from "#core/config/paths.js";
|
|
20
20
|
import { nowIso } from "#core/util/time.js";
|
|
21
|
+
import { readJson } from "#core/util/json-file.js";
|
|
21
22
|
|
|
22
23
|
export const PENDING_CALLBACKS_DIR = path.join(APX_HOME, "pending-callbacks");
|
|
23
24
|
|
|
@@ -63,7 +64,7 @@ export function listPendingCallbacks() {
|
|
|
63
64
|
const out = [];
|
|
64
65
|
for (const f of files) {
|
|
65
66
|
try {
|
|
66
|
-
const entry =
|
|
67
|
+
const entry = readJson(path.join(PENDING_CALLBACKS_DIR, f), null);
|
|
67
68
|
if (entry?.session_id) out.push(entry);
|
|
68
69
|
} catch {
|
|
69
70
|
/* skip corrupt */
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
// JSON files on disk — one implementation.
|
|
2
|
+
//
|
|
3
|
+
// Six modules had grown their own read/write pair and 35 more sites inlined
|
|
4
|
+
// `JSON.parse(fs.readFileSync(...))`. They did not agree on the things that
|
|
5
|
+
// matter, so the guarantees your data got depended on which copy happened to
|
|
6
|
+
// touch it:
|
|
7
|
+
//
|
|
8
|
+
// - Atomicity. Only core/stores/code-sessions.js wrote through a temp file
|
|
9
|
+
// and renamed. Everywhere else, a crash or a full disk mid-write left a
|
|
10
|
+
// truncated file — and since the readers swallow parse errors, the next
|
|
11
|
+
// read silently returned "empty" instead of "corrupt". routines.json and
|
|
12
|
+
// the MCP stores were exposed to that.
|
|
13
|
+
// - Permissions. vars and mcp chmod 0600 because they hold tokens; the
|
|
14
|
+
// others did not, so the same kind of data could land world-readable
|
|
15
|
+
// depending on the writer.
|
|
16
|
+
// - Missing-file semantics. Some returned null, some {}, some threw.
|
|
17
|
+
//
|
|
18
|
+
// Reads are forgiving by design (a missing or corrupt file yields the caller's
|
|
19
|
+
// fallback), because every caller here treats "no data" as a valid state.
|
|
20
|
+
// Writes are strict: they create the directory, write atomically, and throw if
|
|
21
|
+
// they cannot.
|
|
22
|
+
import fs from "node:fs";
|
|
23
|
+
import fsp from "node:fs/promises";
|
|
24
|
+
import path from "node:path";
|
|
25
|
+
|
|
26
|
+
/** Mode for files that may contain tokens or credentials. */
|
|
27
|
+
export const SECRET_MODE = 0o600;
|
|
28
|
+
|
|
29
|
+
// Twelve call sites still parse inline, on purpose. They are the ones whose
|
|
30
|
+
// failure mode is not "fall back to empty":
|
|
31
|
+
//
|
|
32
|
+
// config/index.js a corrupt ~/.apx/config.json must THROW, loudly. Its
|
|
33
|
+
// silent fallback would be an empty config, i.e. every
|
|
34
|
+
// key reset — the worst possible recovery.
|
|
35
|
+
// mcp/sources.js, these report *which* file failed and why, so the user
|
|
36
|
+
// integrations/sources can fix it; a bare fallback would hide the problem.
|
|
37
|
+
// profiles/lifecycle, these read package.json / manifests where a parse
|
|
38
|
+
// project-config, … error is a real error, not a missing-file state.
|
|
39
|
+
//
|
|
40
|
+
// If you are adding a new read whose answer on failure is "treat it as empty",
|
|
41
|
+
// use readJson. If it is "tell the user", keep it explicit.
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Read and parse a JSON file.
|
|
45
|
+
*
|
|
46
|
+
* @param {string} file
|
|
47
|
+
* @param {unknown} [fallback=null] returned when the file is missing, empty or
|
|
48
|
+
* unparseable. Pass `{}` or `[]` when the caller wants a shape it can spread.
|
|
49
|
+
*/
|
|
50
|
+
export function readJson(file, fallback = null) {
|
|
51
|
+
let raw;
|
|
52
|
+
try {
|
|
53
|
+
raw = fs.readFileSync(file, "utf8");
|
|
54
|
+
} catch {
|
|
55
|
+
return fallback; // missing file is a normal state, not an error
|
|
56
|
+
}
|
|
57
|
+
if (!raw.trim()) return fallback;
|
|
58
|
+
try {
|
|
59
|
+
return JSON.parse(raw);
|
|
60
|
+
} catch {
|
|
61
|
+
return fallback;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Async variant, for request paths that must not block the event loop. */
|
|
66
|
+
export async function readJsonAsync(file, fallback = null) {
|
|
67
|
+
let raw;
|
|
68
|
+
try {
|
|
69
|
+
raw = await fsp.readFile(file, "utf8");
|
|
70
|
+
} catch {
|
|
71
|
+
return fallback;
|
|
72
|
+
}
|
|
73
|
+
if (!raw.trim()) return fallback;
|
|
74
|
+
try {
|
|
75
|
+
return JSON.parse(raw);
|
|
76
|
+
} catch {
|
|
77
|
+
return fallback;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Write a value as pretty JSON, atomically.
|
|
83
|
+
*
|
|
84
|
+
* Writes to `<file>.<pid>.tmp` and renames, so a reader never observes a
|
|
85
|
+
* half-written file and a crash cannot destroy the previous contents.
|
|
86
|
+
*
|
|
87
|
+
* @param {string} file
|
|
88
|
+
* @param {unknown} value
|
|
89
|
+
* @param {{ mode?: number }} [opts] `mode: SECRET_MODE` for files holding
|
|
90
|
+
* credentials.
|
|
91
|
+
*/
|
|
92
|
+
export function writeJson(file, value, { mode } = {}) {
|
|
93
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
94
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
95
|
+
try {
|
|
96
|
+
fs.writeFileSync(tmp, JSON.stringify(value, null, 2) + "\n");
|
|
97
|
+
if (mode !== undefined) fs.chmodSync(tmp, mode);
|
|
98
|
+
fs.renameSync(tmp, file);
|
|
99
|
+
} catch (err) {
|
|
100
|
+
// Never leave the temp file behind on a failed write.
|
|
101
|
+
try { fs.unlinkSync(tmp); } catch { /* already gone */ }
|
|
102
|
+
throw err;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Async variant. Same atomic rename. */
|
|
107
|
+
export async function writeJsonAsync(file, value, { mode } = {}) {
|
|
108
|
+
await fsp.mkdir(path.dirname(file), { recursive: true });
|
|
109
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
110
|
+
try {
|
|
111
|
+
await fsp.writeFile(tmp, JSON.stringify(value, null, 2) + "\n");
|
|
112
|
+
if (mode !== undefined) await fsp.chmod(tmp, mode);
|
|
113
|
+
await fsp.rename(tmp, file);
|
|
114
|
+
} catch (err) {
|
|
115
|
+
try { await fsp.unlink(tmp); } catch { /* already gone */ }
|
|
116
|
+
throw err;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Read, transform, write back — atomically, with the same fallback rules.
|
|
122
|
+
* Returns the value that was written.
|
|
123
|
+
*/
|
|
124
|
+
export function updateJson(file, updater, { fallback = null, mode } = {}) {
|
|
125
|
+
const next = updater(readJson(file, fallback));
|
|
126
|
+
writeJson(file, next, { mode });
|
|
127
|
+
return next;
|
|
128
|
+
}
|