@xopcai/xopc 0.0.158 → 0.0.160
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser-ext/manifest.json +1 -1
- package/dist/extensions/dashscope/src/image-generation-provider.js +1 -1
- package/dist/extensions/fal/src/image-generation-provider.js +1 -1
- package/dist/extensions/google/src/image-generation-provider.js +1 -1
- package/dist/extensions/telegram/xopc.extension.json +1 -1
- package/dist/gateway/static/root/assets/{action-boundary-settings-panel-2p1f4-KA.js → action-boundary-settings-panel-CY3sizqC.js} +1 -1
- package/dist/gateway/static/root/assets/{activity-store-C-57rvkX.js → activity-store-3Cso_2qm.js} +1 -1
- package/dist/gateway/static/root/assets/{agent-avatar-dicebear-BIl6X2b_.js → agent-avatar-dicebear-B_27aaZQ.js} +1 -1
- package/dist/gateway/static/root/assets/agents-B1l3iii6.js +2 -0
- package/dist/gateway/static/root/assets/{api-BjcBIV2O.js → api-BbzB57CJ.js} +1 -1
- package/dist/gateway/static/root/assets/{app-management-settings-panel-CZxw161D.js → app-management-settings-panel-DfmyWcyh.js} +1 -1
- package/dist/gateway/static/root/assets/{appearance-settings-CRXNlDhH.js → appearance-settings-B2v0c-K-.js} +1 -1
- package/dist/gateway/static/root/assets/apps-page-DaNFLsp0.js +1 -0
- package/dist/gateway/static/root/assets/{archive-plugin-CpVshjXu.js → archive-plugin-DL5Lnx0C.js} +1 -1
- package/dist/gateway/static/root/assets/{attachment-load-M9WqqJnQ.js → attachment-load-InuP-ZjQ.js} +1 -1
- package/dist/gateway/static/root/assets/{automation-suggestion-card-CYMsAsYf.js → automation-suggestion-card-CahGuh7B.js} +1 -1
- package/dist/gateway/static/root/assets/automations-page-Cd0gMx6p.js +6 -0
- package/dist/gateway/static/root/assets/{binary-plugins-Cjn3zb4u.js → binary-plugins-CdOTGYVu.js} +1 -1
- package/dist/gateway/static/root/assets/{block-editor-BPKdquaV.js → block-editor-D6ShK1S-.js} +3 -3
- package/dist/gateway/static/root/assets/browser-settings-page-BmUYDL2Z.js +8 -0
- package/dist/gateway/static/root/assets/{capability-presets-api-Bzkcsyzn.js → capability-presets-api-BzoLm52Z.js} +1 -1
- package/dist/gateway/static/root/assets/{capability-presets-settings-panel-D4rH4Dax.js → capability-presets-settings-panel-CE7mrWtS.js} +2 -2
- package/dist/gateway/static/root/assets/{channel-recipient-api-CFqrSi5K.js → channel-recipient-api-DbgydMjl.js} +1 -1
- package/dist/gateway/static/root/assets/channels-settings-D1_gcAHK.js +1 -0
- package/dist/gateway/static/root/assets/channels-status-swr-Cqfqv59m.js +1 -0
- package/dist/gateway/static/root/assets/{connector-logo-ljPKdYKh.js → connector-logo-C3bJC9nt.js} +1 -1
- package/dist/gateway/static/root/assets/connectors-page-CaLBi6Na.js +2 -0
- package/dist/gateway/static/root/assets/desktop-pet-settings-BrHVvbBw.js +1 -0
- package/dist/gateway/static/root/assets/desktop-pet-sprite-CFd1jGLJ.js +1 -0
- package/dist/gateway/static/root/assets/desktop-pet-wacrnJJ1.js +1 -0
- package/dist/gateway/static/root/assets/{directory-picker-path-field-Dmy1035j.js → directory-picker-path-field-BPI969w-.js} +1 -1
- package/dist/gateway/static/root/assets/{dreaming-settings-tT4WkBkd.js → dreaming-settings-iw6QXD45.js} +3 -3
- package/dist/gateway/static/root/assets/{extension-debug-page-CL79q8AH.js → extension-debug-page-rCHyhEr6.js} +1 -1
- package/dist/gateway/static/root/assets/{extension-page-hq80wkFI.js → extension-page-C4gQsAPt.js} +1 -1
- package/dist/gateway/static/root/assets/{extension-settings-page-BV-9GDfT.js → extension-settings-page-DB_3eik7.js} +1 -1
- package/dist/gateway/static/root/assets/{gateway-config-api-CFqoN2NW.js → gateway-config-api-Cdt38iF2.js} +1 -1
- package/dist/gateway/static/root/assets/gateway-settings-DM4BgyGw.js +1 -0
- package/dist/gateway/static/root/assets/{gateway-startup-retry-cxQ94qMm.js → gateway-startup-retry-ChRFNjj_.js} +1 -1
- package/dist/gateway/static/root/assets/goal-create-dialog-BQamcEmF.js +1 -0
- package/dist/gateway/static/root/assets/goal-detail-page-DZUQ0Lf1.js +4 -0
- package/dist/gateway/static/root/assets/{goals-config-api--qJ4qlRq.js → goals-config-api-RvljeEm6.js} +1 -1
- package/dist/gateway/static/root/assets/goals-page-BtxVD27C.js +2 -0
- package/dist/gateway/static/root/assets/{goals-settings-h7x9h5lb.js → goals-settings-DMGV4S4L.js} +1 -1
- package/dist/gateway/static/root/assets/{heartbeat-settings-71IhhM3s.js → heartbeat-settings-B8r5EXc5.js} +1 -1
- package/dist/gateway/static/root/assets/index-CHKe7nwC.js +197 -0
- package/dist/gateway/static/root/assets/index-CXhfUIjh.css +1 -0
- package/dist/gateway/static/root/assets/{keyboard-shortcuts-settings-BZGmHvLu.js → keyboard-shortcuts-settings-1-LYibQ4.js} +1 -1
- package/dist/gateway/static/root/assets/local-app-workbench-page-CTTfyFe3.js +2 -0
- package/dist/gateway/static/root/assets/{local-apps-page-CiLqrMIh.js → local-apps-page-CC4vtUUI.js} +1 -1
- package/dist/gateway/static/root/assets/{locale-store-C_M71MK9.js → locale-store-CPbGB_6l.js} +1 -1
- package/dist/gateway/static/root/assets/logs-page-Bn_taxBV.js +7 -0
- package/dist/gateway/static/root/assets/{markdown-split-r1HPWfCU.js → markdown-split-CprEIf8A.js} +1 -1
- package/dist/gateway/static/root/assets/{media-plugins-D74kd3hn.js → media-plugins-BXt_Sj2O.js} +1 -1
- package/dist/gateway/static/root/assets/{messages-BuT0SR1R.js → messages-DDsiqdJS.js} +4 -4
- package/dist/gateway/static/root/assets/models-hub-panel-DJReXRP9.js +2 -0
- package/dist/gateway/static/root/assets/{notes-page-Dgu02514.js → notes-page-Wf5-eyyM.js} +1 -1
- package/dist/gateway/static/root/assets/notes-workbench-BbdjzYzs.js +4 -0
- package/dist/gateway/static/root/assets/{page-tabs-hinRdDy3.js → page-tabs-dtpK_4Fs.js} +1 -1
- package/dist/gateway/static/root/assets/{preview-open-alternatives-C7fw3oGy.js → preview-open-alternatives-DijtEVxG.js} +1 -1
- package/dist/gateway/static/root/assets/project-detail-page-BwG8SOzI.js +1 -0
- package/dist/gateway/static/root/assets/projects-page-nUlyUtxh.js +1 -0
- package/dist/gateway/static/root/assets/remote-access-hub-rf7bjije.js +1 -0
- package/dist/gateway/static/root/assets/{save-bar-controls-C3aU8-vO.js → save-bar-controls-qhDisdqY.js} +1 -1
- package/dist/gateway/static/root/assets/{schedule-field-DjegExFD.js → schedule-field-AhJ50NoT.js} +1 -1
- package/dist/gateway/static/root/assets/{schema-form-DLubnsql.js → schema-form-AuJg2CXU.js} +1 -1
- package/dist/gateway/static/root/assets/sessions-page-CSoHUe38.js +1 -0
- package/dist/gateway/static/root/assets/settings-advanced-gate-Db976IEa.js +1 -0
- package/dist/gateway/static/root/assets/{settings-collapsible-section-3syQHsNR.js → settings-collapsible-section-D5ganr_X.js} +1 -1
- package/dist/gateway/static/root/assets/{settings-form-section-D4DCZzKg.js → settings-form-section-Db3cKRsg.js} +1 -1
- package/dist/gateway/static/root/assets/{settings-loading-skeleton-c5K85ylS.js → settings-loading-skeleton-D05YKT2x.js} +1 -1
- package/dist/gateway/static/root/assets/{settings-page-DNiDlaRq.js → settings-page-DYfXmfTx.js} +1 -1
- package/dist/gateway/static/root/assets/{settings-page-layout-iTRY1Hy-.js → settings-page-layout-Cv_Cu4I5.js} +1 -1
- package/dist/gateway/static/root/assets/setup-status-panel-wBWOFpvw.js +1 -0
- package/dist/gateway/static/root/assets/{share-preview-page-B88Afrhe.js → share-preview-page-CYkZVuse.js} +1 -1
- package/dist/gateway/static/root/assets/shares-settings-BAMma1-P.js +1 -0
- package/dist/gateway/static/root/assets/skill-reload-api-TWxFFWxM.js +1 -0
- package/dist/gateway/static/root/assets/skills-page-Ca2RA29R.js +2 -0
- package/dist/gateway/static/root/assets/{system-settings-panel-DtpTq837.js → system-settings-panel-C1Ng5bXh.js} +1 -1
- package/dist/gateway/static/root/assets/{theme-store-C8ohIHhw.js → theme-store-LMHIDR9n.js} +1 -1
- package/dist/gateway/static/root/assets/{typed-models-lib-ndEnUP3b.js → typed-models-lib-Ch2Bdm_5.js} +1 -1
- package/dist/gateway/static/root/assets/{use-channel-catalog-BWzou5ar.js → use-channel-catalog-CZnaMOeL.js} +1 -1
- package/dist/gateway/static/root/assets/user-context-page-CTumZa0y.js +1 -0
- package/dist/gateway/static/root/assets/voice-api-key-field-DdlMxeqO.js +1 -0
- package/dist/gateway/static/root/assets/work-discovery-page-CcfZhdBi.js +1 -0
- package/dist/gateway/static/root/assets/work-item-detail-page--wxLw1p_.js +1 -0
- package/dist/gateway/static/root/assets/work-page-DyS0XBpV.js +1 -0
- package/dist/gateway/static/root/assets/{workflow-run-setup-panel-ukmpkj66.js → workflow-run-setup-panel-BFk8TvTw.js} +2 -2
- package/dist/gateway/static/root/assets/workflows-page-CVVJd4gy.js +11 -0
- package/dist/gateway/static/root/index.html +7 -7
- package/dist/package.js +1 -1
- package/dist/src/agent/embedded/session-tool-result-guard.js +14 -0
- package/dist/src/agent/image/generation/openai-compatible-image-provider.js +1 -1
- package/dist/src/agent/service/process-direct-streaming.js +1 -1
- package/dist/src/agent/tools/image-generate-tool.js +10 -3
- package/dist/src/agent/tools/send-media.js +20 -70
- package/dist/src/agent/tools/tool-media.d.ts +9 -0
- package/dist/src/agent/tools/tool-media.js +58 -0
- package/dist/src/automations/service/action-executor.js +1 -1
- package/dist/src/cli/commands/doctor/checks/database-schema.js +1 -1
- package/dist/src/config/agent-profile.js +1 -1
- package/dist/src/gateway/agent-run-relay.d.ts +2 -0
- package/dist/src/gateway/agent-run-relay.js +114 -14
- package/dist/src/gateway/agents-admin.js +1 -1
- package/dist/src/gateway/chat-stream/mapper.d.ts +1 -0
- package/dist/src/gateway/chat-stream/mapper.js +40 -19
- package/dist/src/gateway/chat-stream/thinking-delta-coalescer.d.ts +7 -0
- package/dist/src/gateway/chat-stream/thinking-delta-coalescer.js +103 -0
- package/dist/src/gateway/hono/routes/home.js +15 -2
- package/dist/src/gateway/hono/routes/work-discovery.js +451 -0
- package/dist/src/gateway/hono/sse.d.ts +1 -1
- package/dist/src/gateway/hono/sse.js +1 -1
- package/dist/src/gateway/service/run-gateway-agent.js +5 -1
- package/dist/src/gateway/service.d.ts +2 -0
- package/dist/src/gateway/service.js +27 -2
- package/dist/src/media/session-references.js +9 -6
- package/dist/src/mobile/notification-service.js +1 -0
- package/dist/src/pets/factory.js +23 -26
- package/dist/src/pets/manifest.d.ts +2 -1
- package/dist/src/pets/manifest.js +15 -8
- package/dist/src/pets/validator.js +8 -3
- package/dist/src/proactive/calendar-signals.d.ts +4 -0
- package/dist/src/proactive/calendar-signals.js +120 -0
- package/dist/src/proactive/focus-service.d.ts +62 -0
- package/dist/src/proactive/focus-service.js +173 -0
- package/dist/src/proactive/index.d.ts +7 -0
- package/dist/src/proactive/index.js +7 -0
- package/dist/src/proactive/insight-repository.d.ts +19 -0
- package/dist/src/proactive/insight-repository.js +106 -0
- package/dist/src/proactive/run-processor.d.ts +19 -0
- package/dist/src/proactive/run-processor.js +125 -0
- package/dist/src/proactive/trial-expiry-reconciler.d.ts +5 -0
- package/dist/src/proactive/trial-expiry-reconciler.js +30 -0
- package/dist/src/proactive/types.d.ts +64 -0
- package/dist/src/proactive/types.js +1 -0
- package/dist/src/proactive/watch-repository.d.ts +28 -0
- package/dist/src/proactive/watch-repository.js +129 -0
- package/dist/src/session/client-history.d.ts +1 -0
- package/dist/src/session/client-history.js +56 -12
- package/dist/src/session/types.d.ts +1 -0
- package/dist/src/storage/sqlite/knowledge-repository.d.ts +1 -0
- package/dist/src/storage/sqlite/knowledge-repository.js +4 -0
- package/dist/src/storage/sqlite/migrations/047_work_discovery_sources.sql +16 -0
- package/dist/src/storage/sqlite/migrations/048_work_understanding_investigations.sql +40 -0
- package/dist/src/storage/sqlite/migrations/049_work_understanding_threads.sql +52 -0
- package/dist/src/storage/sqlite/migrations/050_work_discovery_source_refreshes.sql +15 -0
- package/dist/src/storage/sqlite/migrations/051_focus_watches.sql +26 -0
- package/dist/src/storage/sqlite/migrations/052_proactive_insights.sql +25 -0
- package/dist/src/storage/sqlite/migrations/runner.d.ts +1 -1
- package/dist/src/storage/sqlite/migrations/runner.js +3 -3
- package/dist/src/storage/sqlite/migrations/runner.ts +1 -1
- package/dist/src/storage/sqlite/schema.js +1 -1
- package/dist/src/work-discovery/analyzer.d.ts +12 -1
- package/dist/src/work-discovery/analyzer.js +126 -7
- package/dist/src/work-discovery/candidate-discovery.d.ts +17 -0
- package/dist/src/work-discovery/candidate-discovery.js +279 -0
- package/dist/src/work-discovery/evaluation.d.ts +11 -0
- package/dist/src/work-discovery/evaluation.js +17 -0
- package/dist/src/work-discovery/governance.d.ts +25 -0
- package/dist/src/work-discovery/governance.js +96 -0
- package/dist/src/work-discovery/incremental-repository.d.ts +13 -0
- package/dist/src/work-discovery/incremental-repository.js +55 -0
- package/dist/src/work-discovery/incremental.d.ts +2 -0
- package/dist/src/work-discovery/incremental.js +7 -0
- package/dist/src/work-discovery/index.d.ts +10 -0
- package/dist/src/work-discovery/index.js +13 -3
- package/dist/src/work-discovery/investigation-repository.d.ts +16 -0
- package/dist/src/work-discovery/investigation-repository.js +122 -0
- package/dist/src/work-discovery/investigator.d.ts +55 -0
- package/dist/src/work-discovery/investigator.js +358 -0
- package/dist/src/work-discovery/probe.d.ts +22 -0
- package/dist/src/work-discovery/probe.js +88 -1
- package/dist/src/work-discovery/service.d.ts +110 -1
- package/dist/src/work-discovery/service.js +578 -8
- package/dist/src/work-discovery/source-repository.d.ts +13 -0
- package/dist/src/work-discovery/source-repository.js +72 -0
- package/dist/src/work-discovery/thread-repository.d.ts +34 -0
- package/dist/src/work-discovery/thread-repository.js +112 -0
- package/dist/src/work-discovery/thread-service.d.ts +8 -0
- package/dist/src/work-discovery/thread-service.js +104 -0
- package/dist/src/work-discovery/types.d.ts +145 -0
- package/package.json +1 -1
- package/skills/creative/hatch-pet/SKILL.md +2 -2
- package/dist/gateway/static/root/assets/agents-bnkKH9EI.js +0 -2
- package/dist/gateway/static/root/assets/apps-page-l4ripRNG.js +0 -1
- package/dist/gateway/static/root/assets/automations-page-Kny7tLiF.js +0 -6
- package/dist/gateway/static/root/assets/browser-settings-page-vr1tXAab.js +0 -8
- package/dist/gateway/static/root/assets/channels-settings-Tlhbi5eh.js +0 -1
- package/dist/gateway/static/root/assets/channels-status-swr-vooEF4lG.js +0 -1
- package/dist/gateway/static/root/assets/connectors-page-_XfvjXoY.js +0 -2
- package/dist/gateway/static/root/assets/desktop-pet-DKUlAgm7.js +0 -1
- package/dist/gateway/static/root/assets/desktop-pet-settings-aWWzjdza.js +0 -1
- package/dist/gateway/static/root/assets/desktop-pet-sprite-DqZ6fbIj.js +0 -1
- package/dist/gateway/static/root/assets/gateway-settings-DzR-kM91.js +0 -1
- package/dist/gateway/static/root/assets/goal-create-dialog-BLjHRtqe.js +0 -1
- package/dist/gateway/static/root/assets/goal-detail-page-BRxeujXh.js +0 -4
- package/dist/gateway/static/root/assets/goals-page-BaZ3aw4u.js +0 -2
- package/dist/gateway/static/root/assets/index-EXa_i6BT.js +0 -193
- package/dist/gateway/static/root/assets/index-gpwk8Nro.css +0 -1
- package/dist/gateway/static/root/assets/local-app-workbench-page-lUpd6lSo.js +0 -2
- package/dist/gateway/static/root/assets/logs-page-FPDTmcWe.js +0 -7
- package/dist/gateway/static/root/assets/models-hub-panel-BXrdePhv.js +0 -2
- package/dist/gateway/static/root/assets/notes-workbench-D-EAhXv1.js +0 -4
- package/dist/gateway/static/root/assets/project-detail-page-BjpB1wrp.js +0 -1
- package/dist/gateway/static/root/assets/projects-page-BxyjqOfC.js +0 -1
- package/dist/gateway/static/root/assets/remote-access-hub-BFv6Upke.js +0 -1
- package/dist/gateway/static/root/assets/sessions-page-BX4DH8J7.js +0 -1
- package/dist/gateway/static/root/assets/settings-advanced-gate-CJ9MbRP8.js +0 -1
- package/dist/gateway/static/root/assets/setup-status-panel-BH4xQKM9.js +0 -1
- package/dist/gateway/static/root/assets/shares-settings-B6kKkvOb.js +0 -1
- package/dist/gateway/static/root/assets/skill-reload-api-DtjS-nUK.js +0 -1
- package/dist/gateway/static/root/assets/skills-page-CQEoPv0u.js +0 -2
- package/dist/gateway/static/root/assets/sliding-segmented-uWmrxE3P.js +0 -1
- package/dist/gateway/static/root/assets/user-context-page-Dn2T3itn.js +0 -1
- package/dist/gateway/static/root/assets/voice-api-key-field-wtWc3SfN.js +0 -1
- package/dist/gateway/static/root/assets/work-discovery-page-Ckxhj66e.js +0 -1
- package/dist/gateway/static/root/assets/work-item-detail-page-_1QgGPNw.js +0 -1
- package/dist/gateway/static/root/assets/work-page-BPdf2iuv.js +0 -1
- package/dist/gateway/static/root/assets/workflows-page-DdKTx0Hs.js +0 -11
|
@@ -6,6 +6,7 @@ import { randomUUID } from "node:crypto";
|
|
|
6
6
|
init_schema();
|
|
7
7
|
init_providers();
|
|
8
8
|
const MAX_MODEL_CONTEXT_CHARS = 12e4;
|
|
9
|
+
const MAX_PERSONAL_CONTEXT_CHARS = 1e5;
|
|
9
10
|
function extractText(content) {
|
|
10
11
|
if (typeof content === "string") return content;
|
|
11
12
|
if (!Array.isArray(content)) return "";
|
|
@@ -66,6 +67,46 @@ function validateSuggestion(value, allowedPaths) {
|
|
|
66
67
|
verification: strings(item.verification, 4).map((entry) => entry.slice(0, 300))
|
|
67
68
|
};
|
|
68
69
|
}
|
|
70
|
+
function validateProfileCandidate(value) {
|
|
71
|
+
if (!value || typeof value !== "object") return null;
|
|
72
|
+
const item = value;
|
|
73
|
+
const category = item.category === "role" || item.category === "focus" || item.category === "technology" || item.category === "workflow" || item.category === "preference" ? item.category : void 0;
|
|
74
|
+
if (!category || typeof item.statement !== "string") return null;
|
|
75
|
+
const statement = item.statement.trim().slice(0, 500);
|
|
76
|
+
if (statement.length < 4) return null;
|
|
77
|
+
const confidence = item.confidence === "high" || item.confidence === "low" ? item.confidence : "medium";
|
|
78
|
+
return {
|
|
79
|
+
id: randomUUID(),
|
|
80
|
+
category,
|
|
81
|
+
statement,
|
|
82
|
+
confidence,
|
|
83
|
+
evidence: strings(item.evidence, 4).map((entry) => entry.slice(0, 300)),
|
|
84
|
+
status: "pending"
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function validateWorkThreadCandidate(value, allowedRefs) {
|
|
88
|
+
if (!value || typeof value !== "object") return null;
|
|
89
|
+
const item = value;
|
|
90
|
+
if (typeof item.title !== "string" || typeof item.summary !== "string") return null;
|
|
91
|
+
const title = item.title.trim().slice(0, 200);
|
|
92
|
+
const summary = item.summary.trim().slice(0, 2e3);
|
|
93
|
+
if (!title || !summary) return null;
|
|
94
|
+
const horizon = item.horizon === "current" || item.horizon === "ongoing" || item.horizon === "long_term" ? item.horizon : "current";
|
|
95
|
+
const status = item.status === "active" || item.status === "paused" || item.status === "blocked" || item.status === "completed" || item.status === "uncertain" ? item.status : "uncertain";
|
|
96
|
+
const confidence = item.confidence === "high" || item.confidence === "low" ? item.confidence : "medium";
|
|
97
|
+
const topicKey = (typeof item.topicKey === "string" ? item.topicKey.toLowerCase() : title.toLowerCase()).replace(/[^a-z0-9\p{L}]+/gu, "-").replace(/^-+|-+$/g, "").slice(0, 100) || `thread-${randomUUID().slice(0, 8)}`;
|
|
98
|
+
const evidenceRefs = strings(item.evidenceRefs, 12).filter((ref) => allowedRefs.has(ref));
|
|
99
|
+
if (!evidenceRefs.length) return null;
|
|
100
|
+
return {
|
|
101
|
+
topicKey,
|
|
102
|
+
title,
|
|
103
|
+
summary,
|
|
104
|
+
horizon,
|
|
105
|
+
status,
|
|
106
|
+
confidence,
|
|
107
|
+
evidenceRefs
|
|
108
|
+
};
|
|
109
|
+
}
|
|
69
110
|
function suggestionScore(suggestion) {
|
|
70
111
|
const confidence = suggestion.confidence === "high" ? 3 : suggestion.confidence === "medium" ? 2 : 1;
|
|
71
112
|
const safety = suggestion.risk === "analysis" ? 3 : suggestion.risk === "command" ? 2 : 1;
|
|
@@ -116,7 +157,14 @@ async function analyzeWorkContext(input) {
|
|
|
116
157
|
content: [
|
|
117
158
|
"You help a user resume real work in one explicitly selected local folder.",
|
|
118
159
|
"Analyze only the supplied bounded snapshot. Never claim that you ran commands, tests, or inspected anything absent from it.",
|
|
119
|
-
"Return only one JSON object with projectSummary, currentState, uncertainties, suggestions, lowConfidence, and contextQuestion.",
|
|
160
|
+
"Return only one JSON object with projectSummary, currentState, uncertainties, suggestions, profileCandidates, workThreads, lowConfidence, and contextQuestion.",
|
|
161
|
+
"profileCandidates contains up to 6 stable, useful facts about the user inferred from the supplied project evidence.",
|
|
162
|
+
"Each profile candidate has category (role, focus, technology, workflow, or preference), statement, confidence, and evidence.",
|
|
163
|
+
"Do not infer sensitive traits, identity, health, finances, political views, or anything not directly supported by the work evidence.",
|
|
164
|
+
"workThreads contains up to 3 evidence-backed work streams with topicKey, title, summary, horizon, status, confidence, and evidenceRefs.",
|
|
165
|
+
"horizon is current, ongoing, or long_term. status is active, paused, blocked, completed, or uncertain.",
|
|
166
|
+
"topicKey is a short stable topic identifier. evidenceRefs must use exact relative paths from the snapshot or git://recent-state.",
|
|
167
|
+
"Distinguish one-off recent edits from work sustained across multiple days. Prefer one current thread and only add ongoing or long_term threads when evidence supports them.",
|
|
120
168
|
"For a normal result, suggestions must contain exactly 3 materially different next steps.",
|
|
121
169
|
"Each suggestion must include actionType, title, rationale, evidence, actionPrompt, confidence, expectedOutcome, estimatedMinutes, risk, and verification.",
|
|
122
170
|
"actionType must be summarize_recent_work, inspect_related_tests, or plan_next_step.",
|
|
@@ -127,6 +175,15 @@ async function analyzeWorkContext(input) {
|
|
|
127
175
|
"If the current objective is unclear or fewer than three credible suggestions exist, set lowConfidence=true, return suggestions=[], and ask one concise contextQuestion.",
|
|
128
176
|
"Use Simplified Chinese for every user-facing string when the visible snapshot documents are mainly Chinese; otherwise use English.",
|
|
129
177
|
"",
|
|
178
|
+
input.candidateContext?.length ? `Other ranked local work projects (metadata only):\n${JSON.stringify(input.candidateContext.slice(0, 8).map((candidate) => ({
|
|
179
|
+
displayName: candidate.displayName,
|
|
180
|
+
projectKind: candidate.projectKind,
|
|
181
|
+
score: candidate.score,
|
|
182
|
+
lastActiveAt: candidate.lastActiveAt,
|
|
183
|
+
branch: candidate.branch,
|
|
184
|
+
changedFileCount: candidate.changedFileCount,
|
|
185
|
+
evidence: candidate.evidence
|
|
186
|
+
})))}` : "",
|
|
130
187
|
JSON.stringify(compactSnapshot)
|
|
131
188
|
].join("\n"),
|
|
132
189
|
timestamp: Date.now()
|
|
@@ -140,17 +197,16 @@ async function analyzeWorkContext(input) {
|
|
|
140
197
|
modelRef,
|
|
141
198
|
result: lowConfidenceResult(input.snapshot, typeof parsed.contextQuestion === "string" ? parsed.contextQuestion.trim().slice(0, 500) : void 0)
|
|
142
199
|
};
|
|
143
|
-
const allowedPaths = new Set([
|
|
144
|
-
|
|
145
|
-
...input.snapshot.documents.map((document) => document.relativePath),
|
|
146
|
-
...input.snapshot.git?.changedPaths ?? []
|
|
147
|
-
]);
|
|
200
|
+
const allowedPaths = new Set([...input.snapshot.documents.map((document) => document.relativePath), ...input.snapshot.git?.changedPaths ?? []]);
|
|
201
|
+
const allowedThreadRefs = new Set([...allowedPaths, "git://recent-state"]);
|
|
148
202
|
const suggestions = Array.isArray(parsed.suggestions) ? parsed.suggestions.map((value) => validateSuggestion(value, allowedPaths)).filter((value) => Boolean(value)) : [];
|
|
149
203
|
if (suggestions.length !== 3) return {
|
|
150
204
|
modelRef,
|
|
151
205
|
result: lowConfidenceResult(input.snapshot)
|
|
152
206
|
};
|
|
153
207
|
const primarySuggestion = [...suggestions].sort((a, b) => suggestionScore(b) - suggestionScore(a))[0];
|
|
208
|
+
const profileCandidates = input.candidateContext?.length && Array.isArray(parsed.profileCandidates) ? parsed.profileCandidates.map(validateProfileCandidate).filter((value) => Boolean(value)).slice(0, 6) : [];
|
|
209
|
+
const workThreadCandidates = Array.isArray(parsed.workThreads) ? parsed.workThreads.map((value) => validateWorkThreadCandidate(value, allowedThreadRefs)).filter((value) => Boolean(value)).slice(0, 3) : [];
|
|
154
210
|
const projectSummary = typeof parsed.projectSummary === "string" ? parsed.projectSummary.trim() : "";
|
|
155
211
|
const currentState = typeof parsed.currentState === "string" ? parsed.currentState.trim() : "";
|
|
156
212
|
if (!projectSummary || !currentState) throw new Error("Analysis result is missing its summary");
|
|
@@ -161,10 +217,73 @@ async function analyzeWorkContext(input) {
|
|
|
161
217
|
currentState: currentState.slice(0, 1200),
|
|
162
218
|
uncertainties: strings(parsed.uncertainties, 6).map((value) => value.slice(0, 500)),
|
|
163
219
|
suggestions,
|
|
220
|
+
...profileCandidates.length > 0 ? { profileCandidates } : {},
|
|
221
|
+
...workThreadCandidates.length > 0 ? { workThreadCandidates } : {},
|
|
164
222
|
...primarySuggestion ? { primarySuggestionId: primarySuggestion.id } : {}
|
|
165
223
|
}
|
|
166
224
|
};
|
|
167
225
|
}
|
|
226
|
+
async function analyzePersonalContext(input) {
|
|
227
|
+
const modelRef = getAgentDefaultModelRef(input.config);
|
|
228
|
+
if (!modelRef) throw new Error("No default model configured");
|
|
229
|
+
let remaining = MAX_PERSONAL_CONTEXT_CHARS;
|
|
230
|
+
const items = input.items.slice(0, 150).flatMap((item, index) => {
|
|
231
|
+
if (remaining <= 0) return [];
|
|
232
|
+
const content = item.content.slice(0, remaining);
|
|
233
|
+
remaining -= content.length;
|
|
234
|
+
return [{
|
|
235
|
+
ref: `${item.source}:${index + 1}`,
|
|
236
|
+
source: item.source,
|
|
237
|
+
title: item.title,
|
|
238
|
+
group: item.group,
|
|
239
|
+
createdAt: item.createdAt,
|
|
240
|
+
modifiedAt: item.modifiedAt,
|
|
241
|
+
startsAt: item.startsAt,
|
|
242
|
+
endsAt: item.endsAt,
|
|
243
|
+
content
|
|
244
|
+
}];
|
|
245
|
+
});
|
|
246
|
+
if (!items.some((item) => item.title.trim() || item.content.trim())) return {
|
|
247
|
+
modelRef,
|
|
248
|
+
profileCandidates: [],
|
|
249
|
+
workThreadCandidates: []
|
|
250
|
+
};
|
|
251
|
+
const prompt = [
|
|
252
|
+
"Analyze the bounded personal work context that the user explicitly chose to connect from Apple Notes, Calendar, and Reminders.",
|
|
253
|
+
"Return only one JSON object with profileCandidates and workThreads.",
|
|
254
|
+
"profileCandidates contains at most 6 stable, useful work-related facts.",
|
|
255
|
+
"Each item has category (role, focus, technology, workflow, or preference), statement, confidence, and evidence.",
|
|
256
|
+
"workThreads contains at most 3 evidence-backed current, ongoing, or long-term work streams.",
|
|
257
|
+
"Each work thread has topicKey, title, summary, status, horizon, confidence, and evidenceRefs.",
|
|
258
|
+
"evidenceRefs must contain only the supplied source:index refs. Do not create a thread without direct support.",
|
|
259
|
+
"Prefer recurring work themes and durable preferences. Do not turn one-off errands or stale notes into user facts.",
|
|
260
|
+
"Do not infer sensitive identity, health, finances, political views, relationships, passwords, credentials, or private contact details.",
|
|
261
|
+
"Evidence must be a short paraphrase and must not quote private note content verbatim.",
|
|
262
|
+
"Calendar and reminder titles may establish commitments and timing, but must not be treated as stable preferences without repeated evidence.",
|
|
263
|
+
"When a work-directory summary is supplied, reconcile it with the personal sources: identify corroboration, conflicts, and whether an apparent focus is current or merely stale.",
|
|
264
|
+
"Do not repeat the directory summary as a personal fact unless at least one supplied personal source supports it.",
|
|
265
|
+
"Use Simplified Chinese when the supplied items are mainly Chinese; otherwise use English.",
|
|
266
|
+
input.workContext ? `Work-directory understanding from the same one-shot investigation:\n${JSON.stringify(input.workContext)}` : "",
|
|
267
|
+
JSON.stringify(items)
|
|
268
|
+
].join("\n");
|
|
269
|
+
const parsed = parseJson(extractText((await completeWithResolvedCredentials(resolveModel(modelRef), { messages: [{
|
|
270
|
+
role: "user",
|
|
271
|
+
content: prompt,
|
|
272
|
+
timestamp: Date.now()
|
|
273
|
+
}] }, {
|
|
274
|
+
maxTokens: 1500,
|
|
275
|
+
temperature: .1,
|
|
276
|
+
signal: input.signal
|
|
277
|
+
})).content));
|
|
278
|
+
if (!parsed) throw new Error("Personal context analysis did not return valid JSON");
|
|
279
|
+
const profileCandidates = Array.isArray(parsed.profileCandidates) ? parsed.profileCandidates.map(validateProfileCandidate).filter((value) => Boolean(value)).slice(0, 6) : [];
|
|
280
|
+
const allowedRefs = new Set(items.map((item) => item.ref));
|
|
281
|
+
return {
|
|
282
|
+
modelRef,
|
|
283
|
+
profileCandidates,
|
|
284
|
+
workThreadCandidates: Array.isArray(parsed.workThreads) ? parsed.workThreads.map((value) => validateWorkThreadCandidate(value, allowedRefs)).filter((value) => Boolean(value)).slice(0, 3) : []
|
|
285
|
+
};
|
|
286
|
+
}
|
|
168
287
|
function workDiscoveryResultMarkdown(result) {
|
|
169
288
|
const lines = [
|
|
170
289
|
`## What I found`,
|
|
@@ -185,4 +304,4 @@ function workDiscoveryResultMarkdown(result) {
|
|
|
185
304
|
return lines.join("\n");
|
|
186
305
|
}
|
|
187
306
|
//#endregion
|
|
188
|
-
export { analyzeWorkContext, workDiscoveryResultMarkdown };
|
|
307
|
+
export { analyzePersonalContext, analyzeWorkContext, workDiscoveryResultMarkdown };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { WorkDiscoveryCandidate } from './types.js';
|
|
2
|
+
export interface DiscoverWorkCandidatesOptions {
|
|
3
|
+
existingProjects?: Array<{
|
|
4
|
+
id: string;
|
|
5
|
+
workspaceRoot?: string;
|
|
6
|
+
}>;
|
|
7
|
+
approvedDirectories?: Array<{
|
|
8
|
+
id: string;
|
|
9
|
+
rootPath: string;
|
|
10
|
+
}>;
|
|
11
|
+
homeDirectory?: string;
|
|
12
|
+
nowMs?: number;
|
|
13
|
+
signal?: AbortSignal;
|
|
14
|
+
platform?: NodeJS.Platform;
|
|
15
|
+
environment?: NodeJS.ProcessEnv;
|
|
16
|
+
}
|
|
17
|
+
export declare function discoverWorkCandidates(options?: DiscoverWorkCandidatesOptions): Promise<WorkDiscoveryCandidate[]>;
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import { inferProjectKind } from "../projects/project-kind.js";
|
|
2
|
+
import "../projects/index.js";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { basename, extname, isAbsolute, join, resolve } from "node:path";
|
|
5
|
+
import { constants } from "node:fs";
|
|
6
|
+
import { execFile } from "node:child_process";
|
|
7
|
+
import { access, readFile, readdir, realpath, stat } from "node:fs/promises";
|
|
8
|
+
import { promisify } from "node:util";
|
|
9
|
+
//#region src/work-discovery/candidate-discovery.ts
|
|
10
|
+
const execFileAsync = promisify(execFile);
|
|
11
|
+
const COMMON_WORK_ROOT_NAMES = [
|
|
12
|
+
"develop",
|
|
13
|
+
"Developer",
|
|
14
|
+
"Projects",
|
|
15
|
+
"workspace",
|
|
16
|
+
"code",
|
|
17
|
+
"src"
|
|
18
|
+
];
|
|
19
|
+
const PERSONAL_WORK_ROOT_NAMES = [
|
|
20
|
+
"Desktop",
|
|
21
|
+
"Documents",
|
|
22
|
+
"Downloads"
|
|
23
|
+
];
|
|
24
|
+
const MAX_ROOT_CHILDREN = 100;
|
|
25
|
+
const GIT_TIMEOUT_MS = 2500;
|
|
26
|
+
const RECENT_GENERAL_WORK_MS = 90 * 864e5;
|
|
27
|
+
const GENERAL_WORK_EXTENSIONS = new Set([
|
|
28
|
+
".doc",
|
|
29
|
+
".docx",
|
|
30
|
+
".md",
|
|
31
|
+
".odt",
|
|
32
|
+
".pdf",
|
|
33
|
+
".ppt",
|
|
34
|
+
".pptx",
|
|
35
|
+
".rtf",
|
|
36
|
+
".txt",
|
|
37
|
+
".xls",
|
|
38
|
+
".xlsx"
|
|
39
|
+
]);
|
|
40
|
+
function abortIfNeeded(signal) {
|
|
41
|
+
if (signal?.aborted) throw new DOMException("Work discovery canceled", "AbortError");
|
|
42
|
+
}
|
|
43
|
+
async function readableDirectory(path) {
|
|
44
|
+
try {
|
|
45
|
+
const canonical = await realpath(path);
|
|
46
|
+
if (!(await stat(canonical)).isDirectory()) return null;
|
|
47
|
+
await access(canonical, constants.R_OK);
|
|
48
|
+
return canonical;
|
|
49
|
+
} catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async function gitSignal(rootPath, signal) {
|
|
54
|
+
try {
|
|
55
|
+
const [{ stdout: statusOutput }, { stdout: commitOutput }] = await Promise.all([execFileAsync("git", [
|
|
56
|
+
"-C",
|
|
57
|
+
rootPath,
|
|
58
|
+
"status",
|
|
59
|
+
"--short",
|
|
60
|
+
"--branch",
|
|
61
|
+
"--untracked-files=no"
|
|
62
|
+
], {
|
|
63
|
+
timeout: GIT_TIMEOUT_MS,
|
|
64
|
+
maxBuffer: 128 * 1024,
|
|
65
|
+
signal
|
|
66
|
+
}), execFileAsync("git", [
|
|
67
|
+
"-C",
|
|
68
|
+
rootPath,
|
|
69
|
+
"log",
|
|
70
|
+
"-1",
|
|
71
|
+
"--format=%ct"
|
|
72
|
+
], {
|
|
73
|
+
timeout: GIT_TIMEOUT_MS,
|
|
74
|
+
maxBuffer: 16 * 1024,
|
|
75
|
+
signal
|
|
76
|
+
})]);
|
|
77
|
+
const statusLines = statusOutput.split("\n").filter(Boolean);
|
|
78
|
+
const branch = statusLines[0]?.startsWith("## ") ? statusLines[0].slice(3).split("...")[0]?.trim() : void 0;
|
|
79
|
+
const timestamp = Number(commitOutput.trim()) * 1e3;
|
|
80
|
+
return {
|
|
81
|
+
...branch ? { branch } : {},
|
|
82
|
+
changedFileCount: Math.min(100, Math.max(0, statusLines.length - 1)),
|
|
83
|
+
...Number.isFinite(timestamp) && timestamp > 0 ? { lastCommitAt: timestamp } : {}
|
|
84
|
+
};
|
|
85
|
+
} catch {
|
|
86
|
+
return { changedFileCount: 0 };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async function recentGeneralWorkAt(rootPath, nowMs) {
|
|
90
|
+
const entries = await readdir(rootPath, { withFileTypes: true }).catch(() => []);
|
|
91
|
+
let latest;
|
|
92
|
+
for (const entry of entries.slice(0, MAX_ROOT_CHILDREN)) {
|
|
93
|
+
if (!entry.isFile() || entry.name.startsWith(".") || !GENERAL_WORK_EXTENSIONS.has(extname(entry.name).toLowerCase())) continue;
|
|
94
|
+
const info = await stat(join(rootPath, entry.name)).catch(() => null);
|
|
95
|
+
if (!info?.isFile() || nowMs - info.mtimeMs > RECENT_GENERAL_WORK_MS) continue;
|
|
96
|
+
latest = latest == null ? info.mtimeMs : Math.max(latest, info.mtimeMs);
|
|
97
|
+
}
|
|
98
|
+
return latest;
|
|
99
|
+
}
|
|
100
|
+
async function isCodingRoot(rootPath) {
|
|
101
|
+
if (inferProjectKind({ workspaceRoot: rootPath }).kind === "coding") return true;
|
|
102
|
+
return stat(join(rootPath, ".git")).then(() => true).catch(() => false);
|
|
103
|
+
}
|
|
104
|
+
function recencyScore(lastActiveAt, nowMs) {
|
|
105
|
+
if (!lastActiveAt) return 0;
|
|
106
|
+
const ageDays = Math.max(0, (nowMs - lastActiveAt) / 864e5);
|
|
107
|
+
if (ageDays <= 2) return 40;
|
|
108
|
+
if (ageDays <= 7) return 34;
|
|
109
|
+
if (ageDays <= 14) return 27;
|
|
110
|
+
if (ageDays <= 30) return 19;
|
|
111
|
+
if (ageDays <= 90) return 10;
|
|
112
|
+
return 2;
|
|
113
|
+
}
|
|
114
|
+
async function probeCandidate(input, nowMs, signal) {
|
|
115
|
+
abortIfNeeded(signal);
|
|
116
|
+
const rootPath = await readableDirectory(input.rootPath);
|
|
117
|
+
if (!rootPath) return null;
|
|
118
|
+
const [rootInfo, git, personalWorkAt] = await Promise.all([
|
|
119
|
+
stat(rootPath),
|
|
120
|
+
gitSignal(rootPath, signal),
|
|
121
|
+
input.source === "personal_work_root" ? recentGeneralWorkAt(rootPath, nowMs) : void 0
|
|
122
|
+
]);
|
|
123
|
+
abortIfNeeded(signal);
|
|
124
|
+
const inference = inferProjectKind({ workspaceRoot: rootPath });
|
|
125
|
+
if (input.source === "common_work_root" && !git.lastCommitAt && inference.kind !== "coding") return null;
|
|
126
|
+
if (input.source === "personal_work_root" && !git.lastCommitAt && inference.kind !== "coding" && !personalWorkAt) return null;
|
|
127
|
+
const lastActiveAt = Math.max(rootInfo.mtimeMs, git.lastCommitAt ?? 0, personalWorkAt ?? 0) || void 0;
|
|
128
|
+
const score = Math.min(100, Math.round(recencyScore(lastActiveAt, nowMs) + (git.lastCommitAt ? 20 : 0) + Math.min(10, git.changedFileCount * 2) + Math.round(inference.confidence * 20)));
|
|
129
|
+
const evidence = [];
|
|
130
|
+
if (git.lastCommitAt) evidence.push("recent Git activity");
|
|
131
|
+
if (git.changedFileCount > 0) evidence.push(`${git.changedFileCount} changed files`);
|
|
132
|
+
if (inference.reasons[0]) evidence.push(inference.reasons[0]);
|
|
133
|
+
if (input.source === "existing_project") evidence.push("already connected to xopc");
|
|
134
|
+
if (input.source === "approved_directory") evidence.push("previously approved work folder");
|
|
135
|
+
return {
|
|
136
|
+
id: input.projectId ? `project:${input.projectId}` : `path:${rootPath}`,
|
|
137
|
+
rootPath,
|
|
138
|
+
displayName: basename(rootPath),
|
|
139
|
+
source: input.source,
|
|
140
|
+
...input.projectId ? { projectId: input.projectId } : {},
|
|
141
|
+
projectKind: inference.kind,
|
|
142
|
+
projectKindConfidence: inference.confidence,
|
|
143
|
+
score,
|
|
144
|
+
...lastActiveAt ? { lastActiveAt } : {},
|
|
145
|
+
...git.branch ? { branch: git.branch } : {},
|
|
146
|
+
changedFileCount: git.changedFileCount,
|
|
147
|
+
evidence: evidence.slice(0, 4)
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
async function commonRootInputs(homeDirectory, signal) {
|
|
151
|
+
const inputs = [];
|
|
152
|
+
for (const name of COMMON_WORK_ROOT_NAMES) {
|
|
153
|
+
abortIfNeeded(signal);
|
|
154
|
+
const commonRoot = await readableDirectory(join(homeDirectory, name));
|
|
155
|
+
if (!commonRoot) continue;
|
|
156
|
+
let entries;
|
|
157
|
+
try {
|
|
158
|
+
entries = await readdir(commonRoot, { withFileTypes: true });
|
|
159
|
+
} catch {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name)).slice(0, MAX_ROOT_CHILDREN)) {
|
|
163
|
+
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
164
|
+
const directPath = join(commonRoot, entry.name);
|
|
165
|
+
if (await isCodingRoot(directPath)) {
|
|
166
|
+
inputs.push({
|
|
167
|
+
rootPath: directPath,
|
|
168
|
+
source: "common_work_root"
|
|
169
|
+
});
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const nestedEntries = await readdir(directPath, { withFileTypes: true }).catch(() => []);
|
|
173
|
+
for (const nested of nestedEntries.sort((left, right) => left.name.localeCompare(right.name)).slice(0, MAX_ROOT_CHILDREN)) {
|
|
174
|
+
abortIfNeeded(signal);
|
|
175
|
+
if (!nested.isDirectory() || nested.name.startsWith(".")) continue;
|
|
176
|
+
const nestedPath = join(directPath, nested.name);
|
|
177
|
+
if (await isCodingRoot(nestedPath)) inputs.push({
|
|
178
|
+
rootPath: nestedPath,
|
|
179
|
+
source: "common_work_root"
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return inputs;
|
|
185
|
+
}
|
|
186
|
+
function resolveXdgPath(value, homeDirectory) {
|
|
187
|
+
const expanded = value.trim().replace(/^"|"$/g, "").replace(/^\$HOME(?=\/|$)|^\$\{HOME\}(?=\/|$)/, homeDirectory);
|
|
188
|
+
return isAbsolute(expanded) ? resolve(expanded) : null;
|
|
189
|
+
}
|
|
190
|
+
async function personalWorkRoots(homeDirectory, platform, environment) {
|
|
191
|
+
const roots = PERSONAL_WORK_ROOT_NAMES.map((name) => join(homeDirectory, name));
|
|
192
|
+
if (platform === "win32") for (const oneDrive of [
|
|
193
|
+
environment.OneDrive,
|
|
194
|
+
environment.OneDriveConsumer,
|
|
195
|
+
environment.OneDriveCommercial
|
|
196
|
+
]) {
|
|
197
|
+
if (!oneDrive) continue;
|
|
198
|
+
roots.push(...PERSONAL_WORK_ROOT_NAMES.map((name) => join(oneDrive, name)));
|
|
199
|
+
}
|
|
200
|
+
if (platform === "linux") {
|
|
201
|
+
const config = await readFile(join(homeDirectory, ".config", "user-dirs.dirs"), "utf8").catch(() => "");
|
|
202
|
+
for (const line of config.split("\n")) {
|
|
203
|
+
const match = /^XDG_(?:DESKTOP|DOCUMENTS|DOWNLOAD)_DIR=(.+)$/.exec(line.trim());
|
|
204
|
+
const path = match?.[1] ? resolveXdgPath(match[1], homeDirectory) : null;
|
|
205
|
+
if (path) roots.push(path);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return [...new Set(roots)];
|
|
209
|
+
}
|
|
210
|
+
async function personalRootInputs(roots, nowMs, signal) {
|
|
211
|
+
const inputs = [];
|
|
212
|
+
for (const root of roots) {
|
|
213
|
+
abortIfNeeded(signal);
|
|
214
|
+
const readableRoot = await readableDirectory(root);
|
|
215
|
+
if (!readableRoot) continue;
|
|
216
|
+
const entries = await readdir(readableRoot, { withFileTypes: true }).catch(() => []);
|
|
217
|
+
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name)).slice(0, MAX_ROOT_CHILDREN)) {
|
|
218
|
+
abortIfNeeded(signal);
|
|
219
|
+
if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
|
|
220
|
+
const directPath = join(readableRoot, entry.name);
|
|
221
|
+
if (await isCodingRoot(directPath) || await recentGeneralWorkAt(directPath, nowMs)) {
|
|
222
|
+
inputs.push({
|
|
223
|
+
rootPath: directPath,
|
|
224
|
+
source: "personal_work_root"
|
|
225
|
+
});
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
const nestedEntries = await readdir(directPath, { withFileTypes: true }).catch(() => []);
|
|
229
|
+
for (const nested of nestedEntries.sort((left, right) => left.name.localeCompare(right.name)).slice(0, MAX_ROOT_CHILDREN)) {
|
|
230
|
+
if (!nested.isDirectory() || nested.name.startsWith(".")) continue;
|
|
231
|
+
const nestedPath = join(directPath, nested.name);
|
|
232
|
+
if (await isCodingRoot(nestedPath) || await recentGeneralWorkAt(nestedPath, nowMs)) inputs.push({
|
|
233
|
+
rootPath: nestedPath,
|
|
234
|
+
source: "personal_work_root"
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return inputs;
|
|
240
|
+
}
|
|
241
|
+
async function discoverWorkCandidates(options = {}) {
|
|
242
|
+
const nowMs = options.nowMs ?? Date.now();
|
|
243
|
+
const homeDirectory = options.homeDirectory ?? homedir();
|
|
244
|
+
const existingInputs = (options.existingProjects ?? []).flatMap((project) => {
|
|
245
|
+
const rootPath = project.workspaceRoot?.trim();
|
|
246
|
+
return rootPath ? [{
|
|
247
|
+
rootPath,
|
|
248
|
+
source: "existing_project",
|
|
249
|
+
projectId: project.id
|
|
250
|
+
}] : [];
|
|
251
|
+
});
|
|
252
|
+
const approvedInputs = (options.approvedDirectories ?? []).map((source) => ({
|
|
253
|
+
rootPath: source.rootPath,
|
|
254
|
+
source: "approved_directory"
|
|
255
|
+
}));
|
|
256
|
+
const [commonInputs, personalInputs] = await Promise.all([commonRootInputs(homeDirectory, options.signal), personalWorkRoots(homeDirectory, options.platform ?? process.platform, options.environment ?? process.env).then((roots) => personalRootInputs(roots, nowMs, options.signal))]);
|
|
257
|
+
const uniqueInputs = /* @__PURE__ */ new Map();
|
|
258
|
+
for (const input of [
|
|
259
|
+
...existingInputs,
|
|
260
|
+
...approvedInputs,
|
|
261
|
+
...commonInputs,
|
|
262
|
+
...personalInputs
|
|
263
|
+
]) {
|
|
264
|
+
const existing = uniqueInputs.get(input.rootPath);
|
|
265
|
+
if (!existing || input.source === "existing_project" || input.source === "approved_directory" && existing.source !== "existing_project") uniqueInputs.set(input.rootPath, input);
|
|
266
|
+
}
|
|
267
|
+
const candidates = [];
|
|
268
|
+
const inputs = [...uniqueInputs.values()];
|
|
269
|
+
for (let index = 0; index < inputs.length; index += 4) {
|
|
270
|
+
abortIfNeeded(options.signal);
|
|
271
|
+
const batch = await Promise.all(inputs.slice(index, index + 4).map((input) => probeCandidate(input, nowMs, options.signal)));
|
|
272
|
+
candidates.push(...batch.filter((item) => Boolean(item)));
|
|
273
|
+
}
|
|
274
|
+
const canonicalCandidates = /* @__PURE__ */ new Map();
|
|
275
|
+
for (const candidate of candidates) if (!canonicalCandidates.get(candidate.rootPath) || candidate.source === "existing_project") canonicalCandidates.set(candidate.rootPath, candidate);
|
|
276
|
+
return [...canonicalCandidates.values()].sort((a, b) => b.score - a.score || (b.lastActiveAt ?? 0) - (a.lastActiveAt ?? 0) || a.rootPath.localeCompare(b.rootPath)).slice(0, 8);
|
|
277
|
+
}
|
|
278
|
+
//#endregion
|
|
279
|
+
export { discoverWorkCandidates };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface WorkUnderstandingEvaluationCase {
|
|
2
|
+
expectedThreadKeys: string[];
|
|
3
|
+
inferredThreadKeys: string[];
|
|
4
|
+
evidenceBackedThreadKeys: string[];
|
|
5
|
+
}
|
|
6
|
+
export declare function evaluateWorkUnderstandingCase(input: WorkUnderstandingEvaluationCase): {
|
|
7
|
+
precision: number;
|
|
8
|
+
recall: number;
|
|
9
|
+
evidenceCoverage: number;
|
|
10
|
+
f1: number;
|
|
11
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//#region src/work-discovery/evaluation.ts
|
|
2
|
+
function evaluateWorkUnderstandingCase(input) {
|
|
3
|
+
const expected = new Set(input.expectedThreadKeys);
|
|
4
|
+
const inferred = new Set(input.inferredThreadKeys);
|
|
5
|
+
const evidenceBacked = new Set(input.evidenceBackedThreadKeys);
|
|
6
|
+
const truePositives = [...inferred].filter((key) => expected.has(key)).length;
|
|
7
|
+
const precision = inferred.size ? truePositives / inferred.size : expected.size ? 0 : 1;
|
|
8
|
+
const recall = expected.size ? truePositives / expected.size : 1;
|
|
9
|
+
return {
|
|
10
|
+
precision,
|
|
11
|
+
recall,
|
|
12
|
+
evidenceCoverage: inferred.size ? [...inferred].filter((key) => evidenceBacked.has(key)).length / inferred.size : 1,
|
|
13
|
+
f1: precision + recall ? 2 * precision * recall / (precision + recall) : 0
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
//#endregion
|
|
17
|
+
export { evaluateWorkUnderstandingCase };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface WorkUnderstandingSourceLineage {
|
|
2
|
+
sourceId: string;
|
|
3
|
+
evidenceCount: number;
|
|
4
|
+
threadCount: number;
|
|
5
|
+
memoryRecordCount: number;
|
|
6
|
+
discoveryRunCount: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function getWorkUnderstandingSourceLineage(sourceId: string): WorkUnderstandingSourceLineage | null;
|
|
9
|
+
export declare function deleteWorkUnderstandingDerivedData(sourceId: string): {
|
|
10
|
+
deletedMemoryRecords: number;
|
|
11
|
+
evidenceCount: number | bigint;
|
|
12
|
+
deletedThreads: number;
|
|
13
|
+
retainedThreads: number;
|
|
14
|
+
};
|
|
15
|
+
export declare function getWorkUnderstandingMetrics(): {
|
|
16
|
+
completedInvestigations: number;
|
|
17
|
+
averageToolCalls: number;
|
|
18
|
+
activeThreads: number;
|
|
19
|
+
confirmedThreads: number;
|
|
20
|
+
recognitionConfirmationRate: number;
|
|
21
|
+
evidenceBySource: {
|
|
22
|
+
sourceType: string;
|
|
23
|
+
count: number;
|
|
24
|
+
}[];
|
|
25
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { requireXopcDatabase } from "../storage/sqlite/connection.js";
|
|
2
|
+
import { runSqliteWriteTransaction } from "../storage/sqlite/transaction.js";
|
|
3
|
+
import { deleteMemoryRecord, listMemoryRecords } from "../storage/sqlite/memory-records-repository.js";
|
|
4
|
+
//#region src/work-discovery/governance.ts
|
|
5
|
+
function memoryRecordsForSource(sourceId, rootPath) {
|
|
6
|
+
const { db } = requireXopcDatabase();
|
|
7
|
+
const runIds = rootPath ? db.prepare("SELECT id FROM work_discovery_runs WHERE root_path = ?").all(rootPath).map((row) => row.id) : [];
|
|
8
|
+
const runPaths = new Set(runIds.map((id) => `work-discovery://${id}`));
|
|
9
|
+
const records = [];
|
|
10
|
+
for (let offset = 0;; offset += 500) {
|
|
11
|
+
const page = listMemoryRecords({
|
|
12
|
+
limit: 500,
|
|
13
|
+
offset
|
|
14
|
+
});
|
|
15
|
+
records.push(...page.filter((record) => runPaths.has(record.source.path)));
|
|
16
|
+
if (page.length < 500) break;
|
|
17
|
+
}
|
|
18
|
+
return records;
|
|
19
|
+
}
|
|
20
|
+
function getWorkUnderstandingSourceLineage(sourceId) {
|
|
21
|
+
const { db } = requireXopcDatabase();
|
|
22
|
+
const source = db.prepare("SELECT root_path FROM work_discovery_sources WHERE source_id = ?").get(sourceId);
|
|
23
|
+
if (!source) return null;
|
|
24
|
+
const evidenceCount = db.prepare("SELECT COUNT(*) AS count FROM work_understanding_evidence WHERE source_grant_id = ?").get(sourceId).count;
|
|
25
|
+
const threadCount = db.prepare(`SELECT COUNT(DISTINCT te.thread_id) AS count
|
|
26
|
+
FROM work_understanding_thread_evidence te
|
|
27
|
+
JOIN work_understanding_evidence e ON e.evidence_id = te.evidence_id
|
|
28
|
+
WHERE e.source_grant_id = ?`).get(sourceId).count;
|
|
29
|
+
const discoveryRunCount = source.root_path ? db.prepare("SELECT COUNT(*) AS count FROM work_discovery_runs WHERE root_path = ?").get(source.root_path).count : 0;
|
|
30
|
+
return {
|
|
31
|
+
sourceId,
|
|
32
|
+
evidenceCount,
|
|
33
|
+
threadCount,
|
|
34
|
+
memoryRecordCount: memoryRecordsForSource(sourceId, source.root_path).length,
|
|
35
|
+
discoveryRunCount
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function deleteWorkUnderstandingDerivedData(sourceId) {
|
|
39
|
+
if (!getWorkUnderstandingSourceLineage(sourceId)) return null;
|
|
40
|
+
const { db } = requireXopcDatabase();
|
|
41
|
+
const memoryRecords = memoryRecordsForSource(sourceId, db.prepare("SELECT root_path FROM work_discovery_sources WHERE source_id = ?").get(sourceId).root_path).filter((record) => record.explicitness !== "explicit");
|
|
42
|
+
const affectedThreadIds = db.prepare(`SELECT DISTINCT te.thread_id AS id
|
|
43
|
+
FROM work_understanding_thread_evidence te
|
|
44
|
+
JOIN work_understanding_evidence e ON e.evidence_id = te.evidence_id
|
|
45
|
+
WHERE e.source_grant_id = ?`).all(sourceId).map((row) => row.id);
|
|
46
|
+
const result = runSqliteWriteTransaction((transaction) => {
|
|
47
|
+
transaction.prepare(`DELETE FROM work_understanding_thread_evidence
|
|
48
|
+
WHERE evidence_id IN (SELECT evidence_id FROM work_understanding_evidence WHERE source_grant_id = ?)`).run(sourceId);
|
|
49
|
+
const evidence = transaction.prepare("DELETE FROM work_understanding_evidence WHERE source_grant_id = ?").run(sourceId);
|
|
50
|
+
let deletedThreads = 0;
|
|
51
|
+
let retainedThreads = 0;
|
|
52
|
+
for (const threadId of affectedThreadIds) {
|
|
53
|
+
if (transaction.prepare("SELECT COUNT(*) AS count FROM work_understanding_thread_evidence WHERE thread_id = ?").get(threadId).count > 0) continue;
|
|
54
|
+
const thread = transaction.prepare("SELECT user_status, status FROM work_understanding_threads WHERE thread_id = ?").get(threadId);
|
|
55
|
+
if (!thread) continue;
|
|
56
|
+
if (thread.user_status === "confirmed" || thread.user_status === "corrected") {
|
|
57
|
+
transaction.prepare(`UPDATE work_understanding_threads SET confidence = MIN(confidence, 0.5),
|
|
58
|
+
status = CASE WHEN status IN ('paused', 'completed') THEN status ELSE 'uncertain' END,
|
|
59
|
+
updated_at = ? WHERE thread_id = ?`).run(Date.now(), threadId);
|
|
60
|
+
retainedThreads += 1;
|
|
61
|
+
} else {
|
|
62
|
+
transaction.prepare("DELETE FROM work_understanding_threads WHERE thread_id = ?").run(threadId);
|
|
63
|
+
deletedThreads += 1;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
evidenceCount: evidence.changes,
|
|
68
|
+
deletedThreads,
|
|
69
|
+
retainedThreads
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
const deletedMemoryRecords = memoryRecords.filter((record) => deleteMemoryRecord(record.id)).length;
|
|
73
|
+
return {
|
|
74
|
+
...result,
|
|
75
|
+
deletedMemoryRecords
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function getWorkUnderstandingMetrics() {
|
|
79
|
+
const { db } = requireXopcDatabase();
|
|
80
|
+
const scalar = (sql) => db.prepare(sql).get().value;
|
|
81
|
+
const evidenceBySource = db.prepare("SELECT source_type AS sourceType, COUNT(*) AS count FROM work_understanding_evidence GROUP BY source_type").all();
|
|
82
|
+
const recognition = db.prepare(`SELECT recognition_decision AS decision, COUNT(*) AS count
|
|
83
|
+
FROM work_discovery_feedback GROUP BY recognition_decision`).all();
|
|
84
|
+
const reviewed = recognition.reduce((sum, item) => sum + (item.decision === "dismissed" ? 0 : item.count), 0);
|
|
85
|
+
const confirmed = recognition.find((item) => item.decision === "confirmed")?.count ?? 0;
|
|
86
|
+
return {
|
|
87
|
+
completedInvestigations: scalar("SELECT COUNT(*) AS value FROM work_understanding_investigations WHERE status = 'completed'"),
|
|
88
|
+
averageToolCalls: scalar("SELECT COALESCE(AVG(tool_call_count), 0) AS value FROM work_understanding_investigations"),
|
|
89
|
+
activeThreads: scalar("SELECT COUNT(*) AS value FROM work_understanding_threads WHERE status IN ('active', 'blocked') AND user_status <> 'rejected'"),
|
|
90
|
+
confirmedThreads: scalar("SELECT COUNT(*) AS value FROM work_understanding_threads WHERE user_status IN ('confirmed', 'corrected')"),
|
|
91
|
+
recognitionConfirmationRate: reviewed ? confirmed / reviewed : 0,
|
|
92
|
+
evidenceBySource
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
//#endregion
|
|
96
|
+
export { deleteWorkUnderstandingDerivedData, getWorkUnderstandingMetrics, getWorkUnderstandingSourceLineage };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { WorkDiscoveryPreview, WorkDiscoverySourceRefresh } from './types.js';
|
|
2
|
+
export declare function recordWorkDiscoverySourceRefresh(input: {
|
|
3
|
+
sourceId: string;
|
|
4
|
+
changed: boolean;
|
|
5
|
+
previousFingerprint?: WorkDiscoveryPreview['fingerprint'];
|
|
6
|
+
currentFingerprint: WorkDiscoveryPreview['fingerprint'];
|
|
7
|
+
status?: WorkDiscoverySourceRefresh['status'];
|
|
8
|
+
discoveryRunId?: string;
|
|
9
|
+
checkedAt?: number;
|
|
10
|
+
}): WorkDiscoverySourceRefresh;
|
|
11
|
+
export declare function listWorkDiscoverySourceRefreshes(sourceId: string, limit?: number): WorkDiscoverySourceRefresh[];
|
|
12
|
+
export declare function findActiveWorkDiscoverySourceRefresh(sourceId: string, fingerprint: WorkDiscoveryPreview['fingerprint']): WorkDiscoverySourceRefresh | null;
|
|
13
|
+
export declare function updateWorkDiscoverySourceRefreshForRun(runId: string, status: 'completed' | 'failed'): void;
|