chatroom-cli 1.89.0 → 1.90.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +552 -149
- package/dist/index.js.map +24 -18
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -77720,13 +77720,15 @@ function renderWebappUxHandoffReference() {
|
|
|
77720
77720
|
'Complete the optional **UX** section in your output when the planner proposes UI changes. Write exactly "Not Applicable." for non-UI tasks. Put code snippets in **Suggested edits** only.',
|
|
77721
77721
|
"",
|
|
77722
77722
|
"1. **Flows** — primary action ≤3 clicks? simpler path exists?",
|
|
77723
|
-
"2. **Patterns** — matches existing components? recommend one if multiple. mobile vs desktop (
|
|
77723
|
+
"2. **Patterns** — matches existing components? recommend one if multiple. mobile vs desktop (responsive variants vs separate mobile UI)?",
|
|
77724
77724
|
"3. **Layout** — compact title+menu row, description, trailing end-aligned CTA? unnecessary wrappers?",
|
|
77725
|
-
"4. **Shortcuts** — consistent with
|
|
77725
|
+
"4. **Shortcuts** — consistent with project conventions? gaps or conflicts?",
|
|
77726
77726
|
"5. **States** — loading spinners/skeletons for async data? error messages on failure? empty states?",
|
|
77727
|
-
"6. **Error boundaries** — risky subtrees wrapped so a throw does not crash the whole app? failure isolated from the
|
|
77728
|
-
"7. **Alignment** — traced parent layout before leaf styles? position/height match siblings?
|
|
77729
|
-
'8. **Feedback** — immediate pending state on async actions (e.g.
|
|
77727
|
+
"6. **Error boundaries** — risky subtrees wrapped so a throw does not crash the whole app? failure isolated from the shell?",
|
|
77728
|
+
"7. **Alignment** — traced parent layout before leaf styles? position/height match siblings?",
|
|
77729
|
+
'8. **Feedback** — immediate pending state on async actions (e.g. save → button "Saving...")?',
|
|
77730
|
+
"9. **Destructive actions** — confirmation dialog before delete/remove/archive/reset/clear or other irreversible/high-impact single actions?",
|
|
77731
|
+
"10. **Bulk actions** — confirmation before batch/multi-item operations (with count or impact summary)?",
|
|
77730
77732
|
"",
|
|
77731
77733
|
"### Flow complexity",
|
|
77732
77734
|
"- Primary action ≤3 clicks from entry point",
|
|
@@ -77735,66 +77737,66 @@ function renderWebappUxHandoffReference() {
|
|
|
77735
77737
|
"- Prefer inline actions over navigate-away-and-back",
|
|
77736
77738
|
"",
|
|
77737
77739
|
"### Presentation & responsive patterns",
|
|
77738
|
-
"- Reuse existing components
|
|
77739
|
-
"- Match badge/button
|
|
77740
|
+
"- Reuse existing design-system components and established UI patterns before introducing new abstractions",
|
|
77741
|
+
"- Match badge/button styling from similar surfaces in the app",
|
|
77740
77742
|
"- When multiple valid patterns exist, recommend one and explain tradeoff",
|
|
77741
|
-
"-
|
|
77742
|
-
"- **Hide/show:**
|
|
77743
|
-
"- **Mobile overlay:**
|
|
77744
|
-
"- **Separate mobile UI:** dedicated mobile modal/picker when desktop uses side panel",
|
|
77745
|
-
"- **Shared responsive density:** same component
|
|
77746
|
-
"- **Command dialogs:**
|
|
77743
|
+
"- Use the project's standard breakpoint(s) for mobile vs desktop",
|
|
77744
|
+
"- **Hide/show:** responsive utility classes or equivalent for alternate chrome per viewport",
|
|
77745
|
+
"- **Mobile overlay:** full-screen or sheet overlay with backdrop when desktop uses persistent panels",
|
|
77746
|
+
"- **Separate mobile UI:** dedicated mobile modal/picker when desktop uses side panel or split view",
|
|
77747
|
+
"- **Shared responsive density:** same component with size/density variants per breakpoint",
|
|
77748
|
+
"- **Command/search dialogs:** match existing modal/dialog styling; sensible max-width on small screens",
|
|
77747
77749
|
"",
|
|
77748
77750
|
"### Layout simplification",
|
|
77749
77751
|
"- Review card/section layouts for unnecessary rows, nested wrappers, or misaligned actions",
|
|
77750
|
-
"- Prefer compact rows: title and overflow menu
|
|
77751
|
-
|
|
77752
|
+
"- Prefer compact rows: title and overflow menu on one line via flex/grid",
|
|
77753
|
+
"- Description on the next line; primary CTA on a trailing row aligned end",
|
|
77752
77754
|
"- Canonical simplified card pattern:",
|
|
77753
77755
|
" ```",
|
|
77754
|
-
" <title> <overflow-menu
|
|
77756
|
+
" <title> <overflow-menu>",
|
|
77755
77757
|
" <description>",
|
|
77756
77758
|
" <primary-cta aligned end>",
|
|
77757
77759
|
" ```",
|
|
77758
|
-
"-
|
|
77759
|
-
"- Flag multi-row chrome that could collapse (menu on its own row, CTA
|
|
77760
|
+
"- Use header + action grid or equivalent flex `justify-between`",
|
|
77761
|
+
"- Flag multi-row chrome that could collapse (menu on its own row, CTA misaligned vs similar cards)",
|
|
77760
77762
|
"",
|
|
77761
77763
|
"### Error & loading states",
|
|
77762
|
-
"- Initial fetch:
|
|
77763
|
-
"- Pagination
|
|
77764
|
-
"- Save mutations: inline success/error
|
|
77764
|
+
"- Initial fetch: centered loader or skeleton for the content area",
|
|
77765
|
+
"- Pagination/infinite scroll: inline loader at scroll edge",
|
|
77766
|
+
"- Save/submit mutations: inline success/error feedback beside the trigger control",
|
|
77765
77767
|
"- Never leave blank panels on fetch failure — show error message or retry affordance",
|
|
77766
|
-
"- Disable interactive controls while
|
|
77768
|
+
"- Disable interactive controls while loading or pending",
|
|
77767
77769
|
"",
|
|
77768
77770
|
"### Error boundaries",
|
|
77769
|
-
"- Wrap data-dependent or third-party subtrees with
|
|
77770
|
-
"-
|
|
77771
|
-
"- Provide fallback UI with recovery action (reload
|
|
77771
|
+
"- Wrap data-dependent or third-party subtrees with error boundaries so a single failure does not unmount the whole app",
|
|
77772
|
+
"- Scope boundaries to the failing panel/section, not the entire shell",
|
|
77773
|
+
"- Provide fallback UI with a recovery action (retry, reload, or navigate away)",
|
|
77772
77774
|
"",
|
|
77773
77775
|
"### Alignment & component hierarchy",
|
|
77774
|
-
"- Before styling a leaf component, trace parent flex/grid
|
|
77775
|
-
"- Match sibling heights and vertical rhythm
|
|
77776
|
+
"- Before styling a leaf component, trace parent flex/grid context",
|
|
77777
|
+
"- Match sibling heights and vertical rhythm",
|
|
77776
77778
|
"- Flag absolute positioning or fixed heights that fight parent layout",
|
|
77777
|
-
"-
|
|
77779
|
+
"- When hierarchy is unclear, inspect the rendered component tree (e.g. DOM inspector or component test snapshot) before deciding leaf styles",
|
|
77778
77780
|
"",
|
|
77779
77781
|
"### Fast user feedback",
|
|
77780
|
-
"- Async actions triggered by keyboard
|
|
77781
|
-
|
|
77782
|
+
"- Async actions triggered by keyboard shortcut or click must show **immediate** UI response",
|
|
77783
|
+
'- Canonical pattern: pending local state → button label changes (e.g. "Saving..."), control disabled while in flight',
|
|
77782
77784
|
"- Show inline error on failure; brief success confirmation optional",
|
|
77783
|
-
|
|
77785
|
+
"- Pair shortcut hints with pending state only when the action shows pending feedback",
|
|
77784
77786
|
"",
|
|
77785
|
-
"###
|
|
77786
|
-
"
|
|
77787
|
-
"
|
|
77788
|
-
"
|
|
77789
|
-
"
|
|
77790
|
-
|
|
77791
|
-
"
|
|
77792
|
-
"
|
|
77793
|
-
"
|
|
77794
|
-
"
|
|
77795
|
-
"
|
|
77796
|
-
"
|
|
77797
|
-
"
|
|
77787
|
+
"### Destructive & bulk action safeguards",
|
|
77788
|
+
"- **Destructive actions** (delete, remove, archive, reset, clear, disable) require an explicit confirmation step — never fire immediately from a menu item or button without a dialog",
|
|
77789
|
+
"- Use the project's standard confirmation dialog/modal pattern",
|
|
77790
|
+
"- Confirmation includes clear title + description of what will happen; primary action styled as destructive when appropriate",
|
|
77791
|
+
"- **Bulk actions** (multi-select delete, batch disable, clear-all) require confirmation before execution — show how many items are affected",
|
|
77792
|
+
'- Bulk confirm should summarize scope (e.g. "Delete 12 items?") and list material impact when non-obvious',
|
|
77793
|
+
"- Flag plans that wire bulk/destructive handlers directly to mutations/API calls without a confirm gate",
|
|
77794
|
+
"",
|
|
77795
|
+
"### Keyboard shortcuts",
|
|
77796
|
+
"- Align proposed shortcuts with the project's existing shortcut catalog and platform conventions (⌘ on macOS, Ctrl on Windows/Linux)",
|
|
77797
|
+
"- Avoid conflicting bindings; document new shortcuts when introducing them",
|
|
77798
|
+
"- Common patterns: modifier+letter for global commands, Enter to confirm in dialogs, Escape to cancel/close, Shift+Enter for multiline input where applicable",
|
|
77799
|
+
"- Flag plans that add shortcuts without checking for conflicts or omit keyboard access for primary actions"
|
|
77798
77800
|
].join(`
|
|
77799
77801
|
`);
|
|
77800
77802
|
}
|
|
@@ -77832,6 +77834,8 @@ function getEnhancerFeedbackTemplateBody() {
|
|
|
77832
77834
|
- **Error boundaries:** <error boundary placement; failure isolated from the whole app>
|
|
77833
77835
|
- **Alignment:** <hierarchy traced; position/height issues; inline snapshot consideration>
|
|
77834
77836
|
- **Feedback:** <immediate pending state on async actions; ⌘Enter + button state>
|
|
77837
|
+
- **Destructive safeguards:** <single-item irreversible/high-impact actions gated by confirm dialog; cite missing confirms>
|
|
77838
|
+
- **Bulk safeguards:** <batch/multi-item operations gated by confirm with count/impact summary; cite missing confirms>
|
|
77835
77839
|
</handoff-ux>
|
|
77836
77840
|
|
|
77837
77841
|
<handoff-notes>
|
|
@@ -78025,6 +78029,28 @@ function getHandoffSeverityGuidanceBlock() {
|
|
|
78025
78029
|
- [low] <minor cleanup — nice to have>`;
|
|
78026
78030
|
}
|
|
78027
78031
|
|
|
78032
|
+
// ../../services/backend/prompts/utils/proof-of-verification.ts
|
|
78033
|
+
function messagesAnchorCommand(params = {}) {
|
|
78034
|
+
const prefix = params.cliEnvPrefix ?? "";
|
|
78035
|
+
const chatroomId = params.chatroomId ?? "<chatroom-id>";
|
|
78036
|
+
const role = params.role ?? "<role>";
|
|
78037
|
+
return `${prefix}chatroom messages anchor --chatroom-id="${chatroomId}" --role="${role}"`;
|
|
78038
|
+
}
|
|
78039
|
+
function getProofOfVerificationComment(params = {}) {
|
|
78040
|
+
const anchorCmd = messagesAnchorCommand(params);
|
|
78041
|
+
return `<!-- Entry-point proof-of-completion workflow — run before filling this section:
|
|
78042
|
+
1. \`${anchorCmd}\` — locate the user's last message (and prior user messages for context)
|
|
78043
|
+
2. \`messages download --since-message-id=<id>\` — download grep-friendly history since anchor; read handoffs and goals
|
|
78044
|
+
3. If the user's last message was terse (e.g. "do it", "raise a PR"), review prior user messages from anchor output and widen --limit before validating
|
|
78045
|
+
4. Validate commits/PRs against ALL requirements — not just the last slice. Incomplete → rework; do NOT hand off to user.
|
|
78046
|
+
Then: ${anchorCmd} → messages download --since-message-id=<id> from anchor output -->`;
|
|
78047
|
+
}
|
|
78048
|
+
function getEntryPointProofOfCompletionVerificationBlock(params = {}) {
|
|
78049
|
+
return `${getProofOfVerificationComment(params)}
|
|
78050
|
+
${PROOF_OF_VERIFICATION_CHECKBOX}`;
|
|
78051
|
+
}
|
|
78052
|
+
var PROOF_OF_VERIFICATION_CHECKBOX = "- [ ] I confirm I verified the user's full request: anchored on the last user message, downloaded history since that anchor, reviewed handoffs/goals (including prior user messages when the latest was a terse follow-up), and validated every requirement below before this handoff";
|
|
78053
|
+
|
|
78028
78054
|
// ../../services/backend/prompts/utils/unresolved-decisions.ts
|
|
78029
78055
|
function getUnresolvedDecisionsSectionBlock() {
|
|
78030
78056
|
return `## Unresolved Decisions
|
|
@@ -78036,7 +78062,7 @@ ${getHandoffSeverityGuidanceBlock()}
|
|
|
78036
78062
|
var init_unresolved_decisions = () => {};
|
|
78037
78063
|
|
|
78038
78064
|
// ../../services/backend/prompts/utils/handoff-report-template-body.ts
|
|
78039
|
-
function getHandoffReportTemplateBody(roleGuidanceContext) {
|
|
78065
|
+
function getHandoffReportTemplateBody(roleGuidanceContext, options = {}) {
|
|
78040
78066
|
return `<handoff-overview>
|
|
78041
78067
|
<!-- For informational tasks (summaries, feedback, Q&A with no code changes): put the complete primary answer in Summary and What changed — the user only sees this handoff. -->
|
|
78042
78068
|
## Summary
|
|
@@ -78061,10 +78087,16 @@ ${getHandoffNotApplicableSectionComment("List planning steps for trivial single-
|
|
|
78061
78087
|
${getHandoffQualityPrinciplesSectionBlock()}
|
|
78062
78088
|
|
|
78063
78089
|
## Proof of Completion
|
|
78090
|
+
${options.includeProofOfVerification ? `${getEntryPointProofOfCompletionVerificationBlock(roleGuidanceContext)}
|
|
78064
78091
|
${getContextReadDisclosureBlock(roleGuidanceContext)}
|
|
78092
|
+
- Context goal: <state the context goal and confirm it was achieved>
|
|
78093
|
+
- Requirements (one bullet per user requirement from the user's message — met/not met + evidence):
|
|
78094
|
+
- <requirement> — <PR URL, commit hash, or file evidence>
|
|
78095
|
+
- Files changed (code tasks — list every file modified):
|
|
78065
78096
|
${getFileReferenceProofOfCompletionExample()}
|
|
78066
|
-
<
|
|
78067
|
-
|
|
78097
|
+
- <additional files as needed>` : `${getContextReadDisclosureBlock(roleGuidanceContext)}
|
|
78098
|
+
${getFileReferenceProofOfCompletionExample()}
|
|
78099
|
+
<evidence the goal was met — list every file you (or the builder) modified>`}
|
|
78068
78100
|
## Backlog Tasks Implemented
|
|
78069
78101
|
${getHandoffNotApplicableSectionComment("List backlog items addressed if none were in scope")}
|
|
78070
78102
|
- \`backlog-item-id\` — <backlog item title/summary and how this work addresses it>
|
|
@@ -78133,7 +78165,7 @@ function getPlannerToUserReportTemplate(roleGuidanceContext) {
|
|
|
78133
78165
|
${getHandoffReportTemplateIntro("Report Template (Planner → User)")}
|
|
78134
78166
|
|
|
78135
78167
|
\`\`\`markdown
|
|
78136
|
-
${getHandoffReportTemplateBody(roleGuidanceContext)}
|
|
78168
|
+
${getHandoffReportTemplateBody(roleGuidanceContext, { includeProofOfVerification: true })}
|
|
78137
78169
|
\`\`\``;
|
|
78138
78170
|
}
|
|
78139
78171
|
var init_planner_to_user = __esm(() => {
|
|
@@ -78176,7 +78208,7 @@ function getSoloToUserReportTemplate(roleGuidanceContext) {
|
|
|
78176
78208
|
${getHandoffReportTemplateIntro("Report Template (Solo → User)")}
|
|
78177
78209
|
|
|
78178
78210
|
\`\`\`markdown
|
|
78179
|
-
${getHandoffReportTemplateBody(roleGuidanceContext)}
|
|
78211
|
+
${getHandoffReportTemplateBody(roleGuidanceContext, { includeProofOfVerification: true })}
|
|
78180
78212
|
\`\`\``;
|
|
78181
78213
|
}
|
|
78182
78214
|
var init_solo_to_user = __esm(() => {
|
|
@@ -78491,12 +78523,16 @@ var init_glossary = __esm(() => {
|
|
|
78491
78523
|
var init_builder = () => {};
|
|
78492
78524
|
// ../../services/backend/prompts/cli/sections/handoff-rules.ts
|
|
78493
78525
|
var init_handoff_rules = () => {};
|
|
78526
|
+
// ../../services/backend/prompts/cli/sections/proof-of-verification.ts
|
|
78527
|
+
var init_proof_of_verification = () => {};
|
|
78528
|
+
|
|
78494
78529
|
// ../../services/backend/prompts/cli/sections/operating-model.ts
|
|
78495
78530
|
var init_operating_model = () => {};
|
|
78496
78531
|
|
|
78497
78532
|
// ../../services/backend/prompts/cli/sections/index.ts
|
|
78498
78533
|
var init_sections = __esm(() => {
|
|
78499
78534
|
init_handoff_rules();
|
|
78535
|
+
init_proof_of_verification();
|
|
78500
78536
|
init_operating_model();
|
|
78501
78537
|
});
|
|
78502
78538
|
|
|
@@ -78506,8 +78542,14 @@ var init_planner = __esm(() => {
|
|
|
78506
78542
|
init_sections();
|
|
78507
78543
|
});
|
|
78508
78544
|
|
|
78545
|
+
// ../../services/backend/prompts/cli/roles/planner-guidance-context.ts
|
|
78546
|
+
var init_planner_guidance_context = __esm(() => {
|
|
78547
|
+
init_env();
|
|
78548
|
+
});
|
|
78549
|
+
|
|
78509
78550
|
// ../../services/backend/prompts/teams/solo/prompts/solo.ts
|
|
78510
78551
|
var init_solo = __esm(() => {
|
|
78552
|
+
init_planner_guidance_context();
|
|
78511
78553
|
init_sections();
|
|
78512
78554
|
});
|
|
78513
78555
|
|
|
@@ -78523,11 +78565,6 @@ var init_builder2 = __esm(() => {
|
|
|
78523
78565
|
init_builder();
|
|
78524
78566
|
});
|
|
78525
78567
|
|
|
78526
|
-
// ../../services/backend/prompts/cli/roles/planner-guidance-context.ts
|
|
78527
|
-
var init_planner_guidance_context = __esm(() => {
|
|
78528
|
-
init_env();
|
|
78529
|
-
});
|
|
78530
|
-
|
|
78531
78568
|
// ../../services/backend/prompts/teams/duo/prompts/planner.ts
|
|
78532
78569
|
var init_planner2 = __esm(() => {
|
|
78533
78570
|
init_planner_guidance_context();
|
|
@@ -80535,40 +80572,56 @@ async function downloadMessages(chatroomId, options, deps) {
|
|
|
80535
80572
|
};
|
|
80536
80573
|
await exports_Effect.runPromise(downloadMessagesEffect(chatroomId, options).pipe(exports_Effect.catchAll(handler), exports_Effect.provide(exports_Layer.mergeAll(layer, MessagesFsServiceLive))));
|
|
80537
80574
|
}
|
|
80538
|
-
var PAGE_SIZE = 50, ABSOLUTE_MAX = 5000, DEFAULT_LIMIT = 10,
|
|
80539
|
-
const
|
|
80540
|
-
|
|
80541
|
-
|
|
80542
|
-
|
|
80543
|
-
|
|
80544
|
-
|
|
80545
|
-
|
|
80546
|
-
|
|
80547
|
-
|
|
80548
|
-
|
|
80549
|
-
|
|
80550
|
-
|
|
80551
|
-
|
|
80552
|
-
|
|
80553
|
-
|
|
80575
|
+
var PAGE_SIZE = 50, SINCE_PAGE_SIZE = 500, ABSOLUTE_MAX = 5000, DEFAULT_LIMIT = 10, fetchForwardSince = (args2, backend2) => exports_Effect.gen(function* () {
|
|
80576
|
+
const messages = [];
|
|
80577
|
+
let cursor = args2.sinceMessageId;
|
|
80578
|
+
let reachedEnd = false;
|
|
80579
|
+
let truncated = false;
|
|
80580
|
+
while (messages.length < args2.maxDownload && !reachedEnd) {
|
|
80581
|
+
const remaining = args2.maxDownload - messages.length;
|
|
80582
|
+
const pageSize = Math.min(SINCE_PAGE_SIZE, remaining);
|
|
80583
|
+
const batch = yield* backend2.query(api.messages.listSinceMessage, {
|
|
80584
|
+
sessionId: args2.sessionId,
|
|
80585
|
+
chatroomId: args2.chatroomId,
|
|
80586
|
+
sinceMessageId: cursor,
|
|
80587
|
+
limit: pageSize
|
|
80588
|
+
});
|
|
80589
|
+
if (batch.length === 0) {
|
|
80590
|
+
reachedEnd = true;
|
|
80591
|
+
break;
|
|
80592
|
+
}
|
|
80593
|
+
const toAppend = cursor === args2.sinceMessageId ? batch : batch.slice(1);
|
|
80594
|
+
if (toAppend.length === 0) {
|
|
80595
|
+
reachedEnd = true;
|
|
80596
|
+
break;
|
|
80597
|
+
}
|
|
80598
|
+
messages.push(...toAppend);
|
|
80599
|
+
if (messages.length >= args2.maxDownload) {
|
|
80600
|
+
truncated = true;
|
|
80601
|
+
break;
|
|
80602
|
+
}
|
|
80603
|
+
cursor = toAppend[toAppend.length - 1]._id;
|
|
80604
|
+
}
|
|
80605
|
+
return { messages, truncated, hasMore: !reachedEnd };
|
|
80606
|
+
}), fetchLatestBackward = (args2, backend2) => exports_Effect.gen(function* () {
|
|
80554
80607
|
const messages = [];
|
|
80555
80608
|
let truncated = false;
|
|
80556
80609
|
let hasMore = true;
|
|
80557
|
-
const firstPageSize = Math.min(PAGE_SIZE, maxDownload);
|
|
80558
|
-
const latest = yield* backend2.query(api.messageList.getLatestMessages, { sessionId, chatroomId, limit: firstPageSize });
|
|
80610
|
+
const firstPageSize = Math.min(PAGE_SIZE, args2.maxDownload);
|
|
80611
|
+
const latest = yield* backend2.query(api.messageList.getLatestMessages, { sessionId: args2.sessionId, chatroomId: args2.chatroomId, limit: firstPageSize });
|
|
80559
80612
|
messages.push(...latest.messages);
|
|
80560
80613
|
hasMore = latest.hasMore;
|
|
80561
|
-
while (hasMore && messages.length < maxDownload) {
|
|
80614
|
+
while (hasMore && messages.length < args2.maxDownload) {
|
|
80562
80615
|
const oldest = messages[0];
|
|
80563
80616
|
if (!oldest) {
|
|
80564
80617
|
hasMore = false;
|
|
80565
80618
|
break;
|
|
80566
80619
|
}
|
|
80567
|
-
const remaining = maxDownload - messages.length;
|
|
80620
|
+
const remaining = args2.maxDownload - messages.length;
|
|
80568
80621
|
const pageSize = Math.min(PAGE_SIZE, remaining);
|
|
80569
80622
|
const batch = yield* backend2.query(api.messageList.listMessagesBefore, {
|
|
80570
|
-
sessionId,
|
|
80571
|
-
chatroomId,
|
|
80623
|
+
sessionId: args2.sessionId,
|
|
80624
|
+
chatroomId: args2.chatroomId,
|
|
80572
80625
|
before: oldest._creationTime,
|
|
80573
80626
|
limit: pageSize
|
|
80574
80627
|
});
|
|
@@ -80576,16 +80629,39 @@ var PAGE_SIZE = 50, ABSOLUTE_MAX = 5000, DEFAULT_LIMIT = 10, downloadMessagesEff
|
|
|
80576
80629
|
hasMore = false;
|
|
80577
80630
|
break;
|
|
80578
80631
|
}
|
|
80579
|
-
const space = maxDownload - messages.length;
|
|
80632
|
+
const space = args2.maxDownload - messages.length;
|
|
80580
80633
|
const toPrepend = batch.slice(Math.max(0, batch.length - space));
|
|
80581
80634
|
for (let i2 = toPrepend.length - 1;i2 >= 0; i2--)
|
|
80582
80635
|
messages.unshift(toPrepend[i2]);
|
|
80583
|
-
hasMore = batch.length >= pageSize && messages.length < maxDownload;
|
|
80584
|
-
if (messages.length >= maxDownload && (batch.length >= pageSize || latest.hasMore))
|
|
80636
|
+
hasMore = batch.length >= pageSize && messages.length < args2.maxDownload;
|
|
80637
|
+
if (messages.length >= args2.maxDownload && (batch.length >= pageSize || latest.hasMore))
|
|
80585
80638
|
truncated = true;
|
|
80586
80639
|
if (toPrepend.length < batch.length)
|
|
80587
80640
|
truncated = true;
|
|
80588
80641
|
}
|
|
80642
|
+
return { messages, truncated, hasMore };
|
|
80643
|
+
}), downloadMessagesEffect = (chatroomId, options) => exports_Effect.gen(function* () {
|
|
80644
|
+
const backend2 = yield* BackendService;
|
|
80645
|
+
const fs11 = yield* MessagesFsService;
|
|
80646
|
+
const sessionId = yield* requireSessionIdEffect((a) => ({
|
|
80647
|
+
_tag: "NotAuthenticated",
|
|
80648
|
+
convexUrl: a.convexUrl,
|
|
80649
|
+
otherUrls: a.otherUrls
|
|
80650
|
+
}));
|
|
80651
|
+
yield* validateChatroomIdEffect(chatroomId, (id3) => ({
|
|
80652
|
+
_tag: "InvalidChatroomId",
|
|
80653
|
+
id: id3
|
|
80654
|
+
}));
|
|
80655
|
+
const format4 = options.format ?? "linear";
|
|
80656
|
+
const maxDownload = parseLimit(options.limit);
|
|
80657
|
+
const outputDir = options.outputDir ?? resolveDownloadOutputDir(format4);
|
|
80658
|
+
const absoluteOutputDir = nodePath2.resolve(outputDir);
|
|
80659
|
+
const { messages, truncated, hasMore } = yield* options.sinceMessageId ? fetchForwardSince({
|
|
80660
|
+
sessionId,
|
|
80661
|
+
chatroomId,
|
|
80662
|
+
sinceMessageId: options.sinceMessageId,
|
|
80663
|
+
maxDownload
|
|
80664
|
+
}, backend2) : fetchLatestBackward({ sessionId, chatroomId, maxDownload }, backend2);
|
|
80589
80665
|
const complete3 = !truncated && !hasMore;
|
|
80590
80666
|
yield* fs11.rm(outputDir, { recursive: true, force: true }).pipe(exports_Effect.catchAll(() => exports_Effect.void));
|
|
80591
80667
|
yield* fs11.mkdir(outputDir, { recursive: true }).pipe(exports_Effect.mapError((cause3) => ({ _tag: "OutputDirError", cause: cause3 })));
|
|
@@ -80613,6 +80689,8 @@ var PAGE_SIZE = 50, ABSOLUTE_MAX = 5000, DEFAULT_LIMIT = 10, downloadMessagesEff
|
|
|
80613
80689
|
count: messages.length,
|
|
80614
80690
|
complete: complete3,
|
|
80615
80691
|
truncated,
|
|
80692
|
+
sinceMessageId: options.sinceMessageId ?? null,
|
|
80693
|
+
anchorMessageId: options.sinceMessageId ?? null,
|
|
80616
80694
|
oldestDownloadedAt: messages[0] ? new Date(messages[0]._creationTime).toISOString() : null,
|
|
80617
80695
|
newestDownloadedAt: messages[messages.length - 1] ? new Date(messages[messages.length - 1]._creationTime).toISOString() : null,
|
|
80618
80696
|
messages: manifestEntries
|
|
@@ -80637,7 +80715,13 @@ var PAGE_SIZE = 50, ABSOLUTE_MAX = 5000, DEFAULT_LIMIT = 10, downloadMessagesEff
|
|
|
80637
80715
|
const nextLimit = Math.min(messages.length * 2, ABSOLUTE_MAX);
|
|
80638
80716
|
console.log(`
|
|
80639
80717
|
\uD83D\uDCA1 Truncated — fetch more history by increasing --limit:`);
|
|
80640
|
-
console.log(` chatroom messages download --chatroom-id=${chatroomId} --role=${options.role} --format=linear --limit=${nextLimit}`);
|
|
80718
|
+
console.log(` chatroom messages download --chatroom-id=${chatroomId} --role=${options.role} --format=linear --limit=${nextLimit}${options.sinceMessageId ? ` --since-message-id=${options.sinceMessageId}` : ""}`);
|
|
80719
|
+
}
|
|
80720
|
+
if (!options.sinceMessageId && !options.limit) {
|
|
80721
|
+
console.log(`
|
|
80722
|
+
\uD83D\uDCA1 Anchor on the user's last message for proof of verification:`);
|
|
80723
|
+
console.log(` chatroom messages anchor --chatroom-id=${chatroomId} --role=${options.role}`);
|
|
80724
|
+
console.log(` Then download history since that anchor: messages download --since-message-id=<id> from anchor output`);
|
|
80641
80725
|
}
|
|
80642
80726
|
});
|
|
80643
80727
|
});
|
|
@@ -80648,6 +80732,108 @@ var init_download = __esm(() => {
|
|
|
80648
80732
|
init_services();
|
|
80649
80733
|
});
|
|
80650
80734
|
|
|
80735
|
+
// src/commands/messages/anchor.ts
|
|
80736
|
+
var exports_anchor = {};
|
|
80737
|
+
__export(exports_anchor, {
|
|
80738
|
+
anchorMessagesEffect: () => anchorMessagesEffect,
|
|
80739
|
+
anchorMessages: () => anchorMessages
|
|
80740
|
+
});
|
|
80741
|
+
function previewContent(content, maxChars = PREVIEW_MAX_CHARS) {
|
|
80742
|
+
const collapsed = content.replace(/\s+/g, " ").trim();
|
|
80743
|
+
return collapsed.length > maxChars ? `${collapsed.slice(0, maxChars)}…` : collapsed;
|
|
80744
|
+
}
|
|
80745
|
+
function formatAnchorTime(creationTime) {
|
|
80746
|
+
return new Date(creationTime).toISOString();
|
|
80747
|
+
}
|
|
80748
|
+
async function anchorMessages(chatroomId, options, deps) {
|
|
80749
|
+
const { getSessionId: getSessionId2, getOtherSessionUrls: getOtherSessionUrls2 } = await Promise.resolve().then(() => (init_storage(), exports_storage));
|
|
80750
|
+
const { getConvexClient: getConvexClient2, getConvexUrl: getConvexUrl2 } = await Promise.resolve().then(() => (init_client2(), exports_client));
|
|
80751
|
+
const client4 = await getConvexClient2();
|
|
80752
|
+
const actualDeps = deps ?? {
|
|
80753
|
+
backend: {
|
|
80754
|
+
mutation: (ep, args2) => client4.mutation(ep, args2),
|
|
80755
|
+
query: (ep, args2) => client4.query(ep, args2)
|
|
80756
|
+
},
|
|
80757
|
+
session: { getSessionId: getSessionId2, getConvexUrl: getConvexUrl2, getOtherSessionUrls: getOtherSessionUrls2 }
|
|
80758
|
+
};
|
|
80759
|
+
const layer = commandServicesLayerFromDeps(actualDeps);
|
|
80760
|
+
const handler = (err) => {
|
|
80761
|
+
return exports_Effect.sync(() => {
|
|
80762
|
+
if (err._tag === "NotAuthenticated") {
|
|
80763
|
+
console.error(`❌ Not authenticated`);
|
|
80764
|
+
process.exit(1);
|
|
80765
|
+
} else if (err._tag === "InvalidChatroomId") {
|
|
80766
|
+
console.error(`❌ Invalid chatroom ID`);
|
|
80767
|
+
process.exit(1);
|
|
80768
|
+
} else if (err._tag === "QueryFailed") {
|
|
80769
|
+
console.error(`
|
|
80770
|
+
❌ Error fetching messages: ${err.cause.message}`);
|
|
80771
|
+
process.exit(1);
|
|
80772
|
+
} else {
|
|
80773
|
+
console.error(`
|
|
80774
|
+
❌ Anchor failed: ${err.message}`);
|
|
80775
|
+
process.exit(1);
|
|
80776
|
+
}
|
|
80777
|
+
});
|
|
80778
|
+
};
|
|
80779
|
+
await exports_Effect.runPromise(anchorMessagesEffect(chatroomId, options).pipe(exports_Effect.catchAll(handler), exports_Effect.provide(layer)));
|
|
80780
|
+
}
|
|
80781
|
+
var PREVIEW_MAX_CHARS = 200, DEFAULT_PRIOR_LIMIT = 3, ANCHOR_DOWNLOAD_DIR = ".chatroom/downloads/messages/linear/anchor", anchorMessagesEffect = (chatroomId, options) => exports_Effect.gen(function* () {
|
|
80782
|
+
const backend2 = yield* BackendService;
|
|
80783
|
+
const sessionId = yield* requireSessionIdEffect((a) => ({
|
|
80784
|
+
_tag: "NotAuthenticated",
|
|
80785
|
+
convexUrl: a.convexUrl,
|
|
80786
|
+
otherUrls: a.otherUrls
|
|
80787
|
+
}));
|
|
80788
|
+
yield* validateChatroomIdEffect(chatroomId, (id3) => ({
|
|
80789
|
+
_tag: "InvalidChatroomId",
|
|
80790
|
+
id: id3
|
|
80791
|
+
}));
|
|
80792
|
+
const priorLimit = options.priorLimit ?? DEFAULT_PRIOR_LIMIT;
|
|
80793
|
+
const result = yield* backend2.query(api.messages.getLastUserMessage, {
|
|
80794
|
+
sessionId,
|
|
80795
|
+
chatroomId,
|
|
80796
|
+
priorLimit
|
|
80797
|
+
}).pipe(exports_Effect.mapError((cause3) => ({ _tag: "QueryFailed", cause: cause3 })));
|
|
80798
|
+
yield* exports_Effect.sync(() => {
|
|
80799
|
+
const { last: last2, prior } = result;
|
|
80800
|
+
if (!last2) {
|
|
80801
|
+
console.log(`
|
|
80802
|
+
\uD83D\uDCED No user messages found in chatroom ${chatroomId}`);
|
|
80803
|
+
return;
|
|
80804
|
+
}
|
|
80805
|
+
console.log(`
|
|
80806
|
+
\uD83C\uDFAF Last user message anchor:`);
|
|
80807
|
+
console.log(` ID: ${last2._id}`);
|
|
80808
|
+
console.log(` Time: ${formatAnchorTime(last2._creationTime)}`);
|
|
80809
|
+
console.log(` Content: ${previewContent(last2.content)}`);
|
|
80810
|
+
if (prior.length > 0) {
|
|
80811
|
+
console.log(`
|
|
80812
|
+
Prior user messages (context for terse follow-ups):`);
|
|
80813
|
+
for (const msg of prior) {
|
|
80814
|
+
console.log(` - ${msg._id} (${formatAnchorTime(msg._creationTime)}) — ${previewContent(msg.content)}`);
|
|
80815
|
+
}
|
|
80816
|
+
}
|
|
80817
|
+
console.log(`
|
|
80818
|
+
✅ Proof of verification workflow:`);
|
|
80819
|
+
console.log(` chatroom messages download --chatroom-id=${chatroomId} --role=${options.role} --since-message-id=${last2._id} --limit=100 --output-dir=${ANCHOR_DOWNLOAD_DIR}`);
|
|
80820
|
+
console.log(` rg "handoff|Goal|Requirements" "${ANCHOR_DOWNLOAD_DIR}/"`);
|
|
80821
|
+
console.log(`
|
|
80822
|
+
\uD83D\uDCA1 If the download reports truncated=true, increase --limit (e.g. 200, 500).`);
|
|
80823
|
+
console.log(`\uD83D\uDCA1 For context before the anchor, download without --since-message-id:`);
|
|
80824
|
+
console.log(` chatroom messages download --chatroom-id=${chatroomId} --role=${options.role} --format=linear --limit=200`);
|
|
80825
|
+
if (prior.length > 0) {
|
|
80826
|
+
console.log(`
|
|
80827
|
+
\uD83D\uDCA1 Terse follow-up (e.g. "do it", "raise a PR")? Review the prior user messages above to recover the full request before validating commits/PRs.`);
|
|
80828
|
+
}
|
|
80829
|
+
});
|
|
80830
|
+
});
|
|
80831
|
+
var init_anchor = __esm(() => {
|
|
80832
|
+
init_esm();
|
|
80833
|
+
init_api3();
|
|
80834
|
+
init_services();
|
|
80835
|
+
});
|
|
80836
|
+
|
|
80651
80837
|
// src/commands/messages/index.ts
|
|
80652
80838
|
var exports_messages = {};
|
|
80653
80839
|
__export(exports_messages, {
|
|
@@ -80655,7 +80841,9 @@ __export(exports_messages, {
|
|
|
80655
80841
|
listSinceMessage: () => listSinceMessage,
|
|
80656
80842
|
listBySenderRoleEffect: () => listBySenderRoleEffect,
|
|
80657
80843
|
listBySenderRole: () => listBySenderRole,
|
|
80658
|
-
downloadMessages: () => downloadMessages
|
|
80844
|
+
downloadMessages: () => downloadMessages,
|
|
80845
|
+
anchorMessagesEffect: () => anchorMessagesEffect,
|
|
80846
|
+
anchorMessages: () => anchorMessages
|
|
80659
80847
|
});
|
|
80660
80848
|
async function createDefaultDeps14() {
|
|
80661
80849
|
const client4 = await getConvexClient();
|
|
@@ -80818,6 +81006,7 @@ var init_messages = __esm(() => {
|
|
|
80818
81006
|
init_client2();
|
|
80819
81007
|
init_services();
|
|
80820
81008
|
init_download();
|
|
81009
|
+
init_anchor();
|
|
80821
81010
|
});
|
|
80822
81011
|
|
|
80823
81012
|
// src/commands/context/format-new-context-error.ts
|
|
@@ -107107,7 +107296,7 @@ var init_assert_registered_working_dir = __esm(() => {
|
|
|
107107
107296
|
|
|
107108
107297
|
// src/infrastructure/services/workspace/workspace-path-security.ts
|
|
107109
107298
|
import { realpath } from "node:fs/promises";
|
|
107110
|
-
import {
|
|
107299
|
+
import { dirname as dirname8, isAbsolute, relative as relative2, resolve as resolve5, sep } from "node:path";
|
|
107111
107300
|
import { gunzipSync } from "node:zlib";
|
|
107112
107301
|
function validateRelativePathSegments(filePath) {
|
|
107113
107302
|
if (filePath.includes("\x00"))
|
|
@@ -107128,6 +107317,39 @@ function isPathInsideRoot(workspaceRoot, targetPath) {
|
|
|
107128
107317
|
return false;
|
|
107129
107318
|
return true;
|
|
107130
107319
|
}
|
|
107320
|
+
function assertContained(workspaceRoot, targetPath) {
|
|
107321
|
+
if (!isPathInsideRoot(workspaceRoot, targetPath)) {
|
|
107322
|
+
return { ok: false, error: "Path escapes workspace" };
|
|
107323
|
+
}
|
|
107324
|
+
return { ok: true };
|
|
107325
|
+
}
|
|
107326
|
+
async function resolveNonExistentTarget(workspaceRoot, candidate) {
|
|
107327
|
+
let current = dirname8(candidate);
|
|
107328
|
+
for (;; ) {
|
|
107329
|
+
try {
|
|
107330
|
+
const ancestorReal = await realpath(current);
|
|
107331
|
+
const ancestorCheck = assertContained(workspaceRoot, ancestorReal);
|
|
107332
|
+
if (!ancestorCheck.ok)
|
|
107333
|
+
return ancestorCheck;
|
|
107334
|
+
const suffix = relative2(current, candidate);
|
|
107335
|
+
const resolved = resolve5(ancestorReal, suffix);
|
|
107336
|
+
const resolvedCheck = assertContained(workspaceRoot, resolved);
|
|
107337
|
+
if (!resolvedCheck.ok)
|
|
107338
|
+
return resolvedCheck;
|
|
107339
|
+
return { ok: true, absolutePath: resolved };
|
|
107340
|
+
} catch (err) {
|
|
107341
|
+
const code2 = err?.code;
|
|
107342
|
+
if (code2 !== "ENOENT") {
|
|
107343
|
+
return { ok: false, error: "Invalid file path" };
|
|
107344
|
+
}
|
|
107345
|
+
const parent = dirname8(current);
|
|
107346
|
+
if (parent === current) {
|
|
107347
|
+
return { ok: false, error: "Path escapes workspace" };
|
|
107348
|
+
}
|
|
107349
|
+
current = parent;
|
|
107350
|
+
}
|
|
107351
|
+
}
|
|
107352
|
+
}
|
|
107131
107353
|
async function resolvePathWithinWorkspace(workingDir, filePath) {
|
|
107132
107354
|
const basic = validateRelativePathSegments(filePath);
|
|
107133
107355
|
if (!basic.ok)
|
|
@@ -107144,32 +107366,16 @@ async function resolvePathWithinWorkspace(workingDir, filePath) {
|
|
|
107144
107366
|
}
|
|
107145
107367
|
try {
|
|
107146
107368
|
const resolved = await realpath(candidate);
|
|
107147
|
-
|
|
107148
|
-
|
|
107149
|
-
|
|
107369
|
+
const containment = assertContained(workspaceRoot, resolved);
|
|
107370
|
+
if (!containment.ok)
|
|
107371
|
+
return containment;
|
|
107150
107372
|
return { ok: true, absolutePath: resolved };
|
|
107151
107373
|
} catch (err) {
|
|
107152
107374
|
const code2 = err?.code;
|
|
107153
107375
|
if (code2 !== "ENOENT") {
|
|
107154
107376
|
return { ok: false, error: "Invalid file path" };
|
|
107155
107377
|
}
|
|
107156
|
-
|
|
107157
|
-
if (parent === workspaceRoot || isPathInsideRoot(workspaceRoot, parent)) {
|
|
107158
|
-
try {
|
|
107159
|
-
const parentReal = await realpath(parent);
|
|
107160
|
-
if (!isPathInsideRoot(workspaceRoot, parentReal)) {
|
|
107161
|
-
return { ok: false, error: "Path escapes workspace" };
|
|
107162
|
-
}
|
|
107163
|
-
const resolved = resolve5(parentReal, basename3(candidate));
|
|
107164
|
-
if (!isPathInsideRoot(workspaceRoot, resolved)) {
|
|
107165
|
-
return { ok: false, error: "Path escapes workspace" };
|
|
107166
|
-
}
|
|
107167
|
-
return { ok: true, absolutePath: resolved };
|
|
107168
|
-
} catch {
|
|
107169
|
-
return { ok: true, absolutePath: candidate };
|
|
107170
|
-
}
|
|
107171
|
-
}
|
|
107172
|
-
return { ok: false, error: "Path escapes workspace" };
|
|
107378
|
+
return resolveNonExistentTarget(workspaceRoot, candidate);
|
|
107173
107379
|
}
|
|
107174
107380
|
}
|
|
107175
107381
|
function gunzipBase64Payload(base643, maxBytes) {
|
|
@@ -108819,10 +109025,10 @@ var init_readdirp = __esm(() => {
|
|
|
108819
109025
|
}
|
|
108820
109026
|
async _formatEntry(dirent, path6) {
|
|
108821
109027
|
let entry;
|
|
108822
|
-
const
|
|
109028
|
+
const basename3 = this._isDirent ? dirent.name : dirent;
|
|
108823
109029
|
try {
|
|
108824
|
-
const fullPath = presolve(pjoin(path6,
|
|
108825
|
-
entry = { path: prelative(this._root, fullPath), fullPath, basename:
|
|
109030
|
+
const fullPath = presolve(pjoin(path6, basename3));
|
|
109031
|
+
entry = { path: prelative(this._root, fullPath), fullPath, basename: basename3 };
|
|
108826
109032
|
entry[this._statsProp] = this._isDirent ? dirent : await this._stat(fullPath);
|
|
108827
109033
|
} catch (err) {
|
|
108828
109034
|
this._onError(err);
|
|
@@ -108909,9 +109115,9 @@ class NodeFsHandler {
|
|
|
108909
109115
|
_watchWithNodeFs(path6, listener) {
|
|
108910
109116
|
const opts = this.fsw.options;
|
|
108911
109117
|
const directory = sp.dirname(path6);
|
|
108912
|
-
const
|
|
109118
|
+
const basename4 = sp.basename(path6);
|
|
108913
109119
|
const parent = this.fsw._getWatchedDir(directory);
|
|
108914
|
-
parent.add(
|
|
109120
|
+
parent.add(basename4);
|
|
108915
109121
|
const absolutePath = sp.resolve(path6);
|
|
108916
109122
|
const options = {
|
|
108917
109123
|
persistent: opts.persistent
|
|
@@ -108921,7 +109127,7 @@ class NodeFsHandler {
|
|
|
108921
109127
|
let closer;
|
|
108922
109128
|
if (opts.usePolling) {
|
|
108923
109129
|
const enableBin = opts.interval !== opts.binaryInterval;
|
|
108924
|
-
options.interval = enableBin && isBinaryPath(
|
|
109130
|
+
options.interval = enableBin && isBinaryPath(basename4) ? opts.binaryInterval : opts.interval;
|
|
108925
109131
|
closer = setFsWatchFileListener(path6, absolutePath, options, {
|
|
108926
109132
|
listener,
|
|
108927
109133
|
rawEmitter: this.fsw._emitRaw
|
|
@@ -108940,10 +109146,10 @@ class NodeFsHandler {
|
|
|
108940
109146
|
return;
|
|
108941
109147
|
}
|
|
108942
109148
|
const dirname10 = sp.dirname(file2);
|
|
108943
|
-
const
|
|
109149
|
+
const basename4 = sp.basename(file2);
|
|
108944
109150
|
const parent = this.fsw._getWatchedDir(dirname10);
|
|
108945
109151
|
let prevStats = stats;
|
|
108946
|
-
if (parent.has(
|
|
109152
|
+
if (parent.has(basename4))
|
|
108947
109153
|
return;
|
|
108948
109154
|
const listener = async (path6, newStats) => {
|
|
108949
109155
|
if (!this.fsw._throttle(THROTTLE_MODE_WATCH, file2, 5))
|
|
@@ -108968,9 +109174,9 @@ class NodeFsHandler {
|
|
|
108968
109174
|
prevStats = newStats2;
|
|
108969
109175
|
}
|
|
108970
109176
|
} catch (error51) {
|
|
108971
|
-
this.fsw._remove(dirname10,
|
|
109177
|
+
this.fsw._remove(dirname10, basename4);
|
|
108972
109178
|
}
|
|
108973
|
-
} else if (parent.has(
|
|
109179
|
+
} else if (parent.has(basename4)) {
|
|
108974
109180
|
const at = newStats.atimeMs;
|
|
108975
109181
|
const mt = newStats.mtimeMs;
|
|
108976
109182
|
if (!at || at <= mt || mt !== prevStats.mtimeMs) {
|
|
@@ -110907,11 +111113,114 @@ var init_file_tree_subscription = __esm(() => {
|
|
|
110907
111113
|
init_daemon_services();
|
|
110908
111114
|
});
|
|
110909
111115
|
|
|
111116
|
+
// ../../services/backend/src/domain/constants/chat-attachment-upload-path.ts
|
|
111117
|
+
function sanitizeChatAttachmentFileName(originalFileName) {
|
|
111118
|
+
const base = originalFileName.replace(/\\/g, "/").split("/").pop() ?? "";
|
|
111119
|
+
const cleaned = base.replace(/[\0-\x1f\x7f"<>\|:*?]/g, "").replace(/[^A-Za-z0-9._-]+/g, "_").replace(/^_+|_+$/g, "");
|
|
111120
|
+
const valid = cleaned.length > 0 && cleaned !== "." && cleaned !== "..";
|
|
111121
|
+
return valid ? cleaned.slice(0, 120) : "attachment";
|
|
111122
|
+
}
|
|
111123
|
+
function getAttachmentRemainder(normalized) {
|
|
111124
|
+
if (!normalized.startsWith(`${CHAT_ATTACHMENT_UPLOAD_DIR}/`))
|
|
111125
|
+
return null;
|
|
111126
|
+
const remainder = normalized.slice(CHAT_ATTACHMENT_UPLOAD_DIR.length + 1);
|
|
111127
|
+
if (!remainder || remainder.includes("/"))
|
|
111128
|
+
return null;
|
|
111129
|
+
return remainder;
|
|
111130
|
+
}
|
|
111131
|
+
function parseAttachmentFileName(remainder) {
|
|
111132
|
+
const underscore = remainder.indexOf("_");
|
|
111133
|
+
if (underscore <= 0)
|
|
111134
|
+
return false;
|
|
111135
|
+
const uuid3 = remainder.slice(0, underscore);
|
|
111136
|
+
const basename5 = remainder.slice(underscore + 1);
|
|
111137
|
+
if (!UUID_V4_RE.test(uuid3) || !basename5)
|
|
111138
|
+
return false;
|
|
111139
|
+
return sanitizeChatAttachmentFileName(basename5) === basename5;
|
|
111140
|
+
}
|
|
111141
|
+
function getInvalidChatAttachmentUploadPathReason(relativePath) {
|
|
111142
|
+
if (relativePath.includes("\\"))
|
|
111143
|
+
return "Invalid attachment path";
|
|
111144
|
+
const normalized = relativePath.replace(/\\/g, "/");
|
|
111145
|
+
const remainder = getAttachmentRemainder(normalized);
|
|
111146
|
+
if (!remainder || !parseAttachmentFileName(remainder)) {
|
|
111147
|
+
return "Invalid attachment path";
|
|
111148
|
+
}
|
|
111149
|
+
if (new TextEncoder().encode(remainder).length > CHAT_ATTACHMENT_FILENAME_MAX_BYTES) {
|
|
111150
|
+
return "Invalid attachment path";
|
|
111151
|
+
}
|
|
111152
|
+
return null;
|
|
111153
|
+
}
|
|
111154
|
+
var CHAT_ATTACHMENT_UPLOAD_DIR = ".chatroom/downloads/attachments/files", UUID_V4_RE, CHAT_ATTACHMENT_FILENAME_MAX_BYTES = 255;
|
|
111155
|
+
var init_chat_attachment_upload_path = __esm(() => {
|
|
111156
|
+
UUID_V4_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
111157
|
+
});
|
|
111158
|
+
|
|
111159
|
+
// ../../services/backend/src/domain/constants/workspace-upload-path-policy.ts
|
|
111160
|
+
function normalizeSegments(relativePath) {
|
|
111161
|
+
const segments = relativePath.replace(/\\/g, "/").split("/").filter(Boolean);
|
|
111162
|
+
const lowerSegments = segments.map((segment) => segment.toLowerCase());
|
|
111163
|
+
return { lowerSegments, basename: lowerSegments.at(-1) ?? "" };
|
|
111164
|
+
}
|
|
111165
|
+
function hasBlockedSegment(lowerSegments) {
|
|
111166
|
+
return lowerSegments.some((segment) => BLOCKED_SEGMENT_NAMES.has(segment));
|
|
111167
|
+
}
|
|
111168
|
+
function hasBlockedBasename(basename5) {
|
|
111169
|
+
return /^\.env(?:\.|$)/.test(basename5) || /\.(?:pem|key)$/.test(basename5) || basename5 === "id_rsa" || basename5 === "credentials.json";
|
|
111170
|
+
}
|
|
111171
|
+
function isBlockedUploadTargetPath(relativePath) {
|
|
111172
|
+
if (!relativePath)
|
|
111173
|
+
return false;
|
|
111174
|
+
const { lowerSegments, basename: basename5 } = normalizeSegments(relativePath);
|
|
111175
|
+
return hasBlockedSegment(lowerSegments) || hasBlockedBasename(basename5);
|
|
111176
|
+
}
|
|
111177
|
+
function getBlockedUploadTargetReason(relativePath) {
|
|
111178
|
+
if (!isBlockedUploadTargetPath(relativePath))
|
|
111179
|
+
return null;
|
|
111180
|
+
return "Cannot upload to this location (.git and sensitive paths are blocked)";
|
|
111181
|
+
}
|
|
111182
|
+
var BLOCKED_SEGMENT_NAMES;
|
|
111183
|
+
var init_workspace_upload_path_policy = __esm(() => {
|
|
111184
|
+
BLOCKED_SEGMENT_NAMES = new Set([".git", "secrets", ".aws"]);
|
|
111185
|
+
});
|
|
111186
|
+
|
|
111187
|
+
// ../../services/backend/src/domain/constants/workspace-upload.ts
|
|
111188
|
+
var MAX_WORKSPACE_UPLOAD_BYTES;
|
|
111189
|
+
var init_workspace_upload = __esm(() => {
|
|
111190
|
+
MAX_WORKSPACE_UPLOAD_BYTES = 70 * 1024 * 1024;
|
|
111191
|
+
});
|
|
111192
|
+
|
|
110910
111193
|
// src/daemon/entry/files/file-write-errors.ts
|
|
110911
111194
|
function unsupportedFileWriteOperationMessage(operation) {
|
|
110912
111195
|
return `Unsupported file write operation "${operation}". ` + "Please upgrade chatroom-cli to the latest version and restart the machine daemon " + "(e.g. npm install -g chatroom-cli@latest).";
|
|
110913
111196
|
}
|
|
110914
111197
|
|
|
111198
|
+
// src/daemon/entry/files/file-write-request-priority.ts
|
|
111199
|
+
function compareFileWriteRequestPriority(a, b) {
|
|
111200
|
+
const rank = (r) => {
|
|
111201
|
+
if (FAST_OPERATIONS.has(r.operation))
|
|
111202
|
+
return 0;
|
|
111203
|
+
if (r.storageId)
|
|
111204
|
+
return 2;
|
|
111205
|
+
return 1;
|
|
111206
|
+
};
|
|
111207
|
+
const diff8 = rank(a) - rank(b);
|
|
111208
|
+
if (diff8 !== 0)
|
|
111209
|
+
return diff8;
|
|
111210
|
+
return a._id.localeCompare(b._id);
|
|
111211
|
+
}
|
|
111212
|
+
function sortPendingFileWriteRequests(requests) {
|
|
111213
|
+
return [...requests].sort(compareFileWriteRequestPriority);
|
|
111214
|
+
}
|
|
111215
|
+
var FAST_OPERATIONS;
|
|
111216
|
+
var init_file_write_request_priority = __esm(() => {
|
|
111217
|
+
FAST_OPERATIONS = new Set([
|
|
111218
|
+
"delete",
|
|
111219
|
+
"mkdir",
|
|
111220
|
+
"rename"
|
|
111221
|
+
]);
|
|
111222
|
+
});
|
|
111223
|
+
|
|
110915
111224
|
// src/daemon/entry/files/file-write-fulfillment.ts
|
|
110916
111225
|
import { access as access4, mkdir as mkdir10, rename as rename4, rm as rm4, writeFile as writeFile9 } from "node:fs/promises";
|
|
110917
111226
|
import { dirname as dirname11 } from "node:path";
|
|
@@ -110928,20 +111237,43 @@ function isTerminalFileWriteError(errorMessage) {
|
|
|
110928
111237
|
"Target path is required for rename",
|
|
110929
111238
|
"Rename target must differ from source path",
|
|
110930
111239
|
"Directory already exists",
|
|
110931
|
-
"Workspace not registered for this machine"
|
|
111240
|
+
"Workspace not registered for this machine",
|
|
111241
|
+
"Cannot upload to this location (.git and sensitive paths are blocked)",
|
|
111242
|
+
"Invalid attachment path"
|
|
110932
111243
|
]);
|
|
110933
111244
|
if (errorMessage.startsWith("Unsupported file write operation"))
|
|
110934
111245
|
return true;
|
|
110935
111246
|
return terminalMessages.has(errorMessage);
|
|
110936
111247
|
}
|
|
110937
|
-
|
|
111248
|
+
function getChatAttachmentValidationError(request2) {
|
|
111249
|
+
if (request2.uploadKind === "chatAttachment") {
|
|
111250
|
+
return getInvalidChatAttachmentUploadPathReason(request2.filePath);
|
|
111251
|
+
}
|
|
111252
|
+
if (request2.storageId && request2.filePath.replace(/\\/g, "/").startsWith(CHAT_ATTACHMENT_RESERVED_PREFIX)) {
|
|
111253
|
+
return getInvalidChatAttachmentUploadPathReason(request2.filePath);
|
|
111254
|
+
}
|
|
111255
|
+
return null;
|
|
111256
|
+
}
|
|
111257
|
+
async function completeWriteRequest(session2, requestId, revision, result) {
|
|
110938
111258
|
await session2.backend.mutation(api.workspaceFiles.completeFileWriteRequest, {
|
|
110939
111259
|
sessionId: session2.sessionId,
|
|
110940
111260
|
requestId,
|
|
111261
|
+
revision,
|
|
110941
111262
|
status: result.status,
|
|
110942
111263
|
errorMessage: result.status === "error" ? result.errorMessage : undefined
|
|
110943
111264
|
});
|
|
110944
111265
|
}
|
|
111266
|
+
async function claimWriteRequest(session2, request2) {
|
|
111267
|
+
const result = await session2.backend.mutation(api.workspaceFiles.claimFileWriteRequest, {
|
|
111268
|
+
sessionId: session2.sessionId,
|
|
111269
|
+
requestId: request2._id,
|
|
111270
|
+
expectedRevision: request2.revision
|
|
111271
|
+
});
|
|
111272
|
+
if (result.status !== "claimed") {
|
|
111273
|
+
return { status: "stale" };
|
|
111274
|
+
}
|
|
111275
|
+
return result;
|
|
111276
|
+
}
|
|
110945
111277
|
async function fileExistsAt(absolutePath) {
|
|
110946
111278
|
return access4(absolutePath).then(() => true).catch(() => false);
|
|
110947
111279
|
}
|
|
@@ -110949,7 +111281,31 @@ function decodeWritePayload(request2) {
|
|
|
110949
111281
|
if (!request2.data) {
|
|
110950
111282
|
return { ok: false, errorMessage: "Missing file data" };
|
|
110951
111283
|
}
|
|
110952
|
-
return gunzipBase64Payload(request2.data.content,
|
|
111284
|
+
return gunzipBase64Payload(request2.data.content, MAX_INLINE_CONTENT_BYTES);
|
|
111285
|
+
}
|
|
111286
|
+
async function fetchStoragePayload(session2, request2) {
|
|
111287
|
+
const storageUrl = await session2.backend.query(api.workspaceFiles.getWriteRequestStorageUrl, {
|
|
111288
|
+
sessionId: session2.sessionId,
|
|
111289
|
+
requestId: request2._id
|
|
111290
|
+
});
|
|
111291
|
+
if (!storageUrl) {
|
|
111292
|
+
return { ok: false, errorMessage: "Missing upload data" };
|
|
111293
|
+
}
|
|
111294
|
+
const response = await fetch(storageUrl);
|
|
111295
|
+
if (!response.ok) {
|
|
111296
|
+
return { ok: false, errorMessage: "Failed to fetch upload" };
|
|
111297
|
+
}
|
|
111298
|
+
const content = Buffer.from(await response.arrayBuffer());
|
|
111299
|
+
if (content.length > MAX_UPLOAD_CONTENT_BYTES) {
|
|
111300
|
+
return { ok: false, errorMessage: "File content too large" };
|
|
111301
|
+
}
|
|
111302
|
+
return { ok: true, content };
|
|
111303
|
+
}
|
|
111304
|
+
async function resolveWritePayload(session2, request2) {
|
|
111305
|
+
if (request2.data?.content) {
|
|
111306
|
+
return decodeWritePayload(request2);
|
|
111307
|
+
}
|
|
111308
|
+
return fetchStoragePayload(session2, request2);
|
|
110953
111309
|
}
|
|
110954
111310
|
async function validateWriteOperation(operation, absolutePath) {
|
|
110955
111311
|
const exists3 = await fileExistsAt(absolutePath);
|
|
@@ -110967,23 +111323,38 @@ async function validateWriteOperation(operation, absolutePath) {
|
|
|
110967
111323
|
async function writePayloadToDisk(absolutePath, operation, content) {
|
|
110968
111324
|
if (operation === "create") {
|
|
110969
111325
|
await mkdir10(dirname11(absolutePath), { recursive: true });
|
|
111326
|
+
await writeFile9(absolutePath, content, { flag: "wx" });
|
|
111327
|
+
return;
|
|
110970
111328
|
}
|
|
110971
111329
|
await writeFile9(absolutePath, content);
|
|
110972
111330
|
}
|
|
110973
111331
|
async function fulfillOneFileWriteRequest(session2, request2) {
|
|
111332
|
+
const claim = await claimWriteRequest(session2, request2);
|
|
111333
|
+
if (claim.status !== "claimed") {
|
|
111334
|
+
return;
|
|
111335
|
+
}
|
|
111336
|
+
const claimed = claim.request;
|
|
110974
111337
|
const startTime = Date.now();
|
|
110975
|
-
const { workingDir, filePath, operation } =
|
|
111338
|
+
const { workingDir, filePath, operation } = claimed;
|
|
110976
111339
|
const registered2 = await assertRegisteredWorkingDir(session2, workingDir);
|
|
110977
111340
|
if (!registered2.ok) {
|
|
110978
|
-
await completeWriteRequest(session2,
|
|
111341
|
+
await completeWriteRequest(session2, claimed._id, claimed.revision, {
|
|
110979
111342
|
status: "error",
|
|
110980
111343
|
errorMessage: registered2.error
|
|
110981
111344
|
});
|
|
110982
111345
|
return;
|
|
110983
111346
|
}
|
|
111347
|
+
const attachmentReason = getChatAttachmentValidationError(claimed);
|
|
111348
|
+
if (attachmentReason) {
|
|
111349
|
+
await completeWriteRequest(session2, claimed._id, claimed.revision, {
|
|
111350
|
+
status: "error",
|
|
111351
|
+
errorMessage: attachmentReason
|
|
111352
|
+
});
|
|
111353
|
+
return;
|
|
111354
|
+
}
|
|
110984
111355
|
const resolved = await resolvePathWithinWorkspace(workingDir, filePath);
|
|
110985
111356
|
if (!resolved.ok) {
|
|
110986
|
-
await completeWriteRequest(session2,
|
|
111357
|
+
await completeWriteRequest(session2, claimed._id, claimed.revision, {
|
|
110987
111358
|
status: "error",
|
|
110988
111359
|
errorMessage: resolved.error
|
|
110989
111360
|
});
|
|
@@ -110991,23 +111362,23 @@ async function fulfillOneFileWriteRequest(session2, request2) {
|
|
|
110991
111362
|
}
|
|
110992
111363
|
try {
|
|
110993
111364
|
if (operation === "rename") {
|
|
110994
|
-
if (!
|
|
110995
|
-
await completeWriteRequest(session2,
|
|
111365
|
+
if (!claimed.targetFilePath) {
|
|
111366
|
+
await completeWriteRequest(session2, claimed._id, claimed.revision, {
|
|
110996
111367
|
status: "error",
|
|
110997
111368
|
errorMessage: "Target path is required for rename"
|
|
110998
111369
|
});
|
|
110999
111370
|
return;
|
|
111000
111371
|
}
|
|
111001
|
-
if (
|
|
111002
|
-
await completeWriteRequest(session2,
|
|
111372
|
+
if (claimed.targetFilePath === filePath) {
|
|
111373
|
+
await completeWriteRequest(session2, claimed._id, claimed.revision, {
|
|
111003
111374
|
status: "error",
|
|
111004
111375
|
errorMessage: "Rename target must differ from source path"
|
|
111005
111376
|
});
|
|
111006
111377
|
return;
|
|
111007
111378
|
}
|
|
111008
|
-
const targetResolved = await resolvePathWithinWorkspace(workingDir,
|
|
111379
|
+
const targetResolved = await resolvePathWithinWorkspace(workingDir, claimed.targetFilePath);
|
|
111009
111380
|
if (!targetResolved.ok) {
|
|
111010
|
-
await completeWriteRequest(session2,
|
|
111381
|
+
await completeWriteRequest(session2, claimed._id, claimed.revision, {
|
|
111011
111382
|
status: "error",
|
|
111012
111383
|
errorMessage: targetResolved.error
|
|
111013
111384
|
});
|
|
@@ -111015,7 +111386,7 @@ async function fulfillOneFileWriteRequest(session2, request2) {
|
|
|
111015
111386
|
}
|
|
111016
111387
|
const sourceExists = await fileExistsAt(resolved.absolutePath);
|
|
111017
111388
|
if (!sourceExists) {
|
|
111018
|
-
await completeWriteRequest(session2,
|
|
111389
|
+
await completeWriteRequest(session2, claimed._id, claimed.revision, {
|
|
111019
111390
|
status: "error",
|
|
111020
111391
|
errorMessage: "File does not exist"
|
|
111021
111392
|
});
|
|
@@ -111023,7 +111394,7 @@ async function fulfillOneFileWriteRequest(session2, request2) {
|
|
|
111023
111394
|
}
|
|
111024
111395
|
const targetExists = await fileExistsAt(targetResolved.absolutePath);
|
|
111025
111396
|
if (targetExists) {
|
|
111026
|
-
await completeWriteRequest(session2,
|
|
111397
|
+
await completeWriteRequest(session2, claimed._id, claimed.revision, {
|
|
111027
111398
|
status: "error",
|
|
111028
111399
|
errorMessage: "Target path already exists"
|
|
111029
111400
|
});
|
|
@@ -111031,29 +111402,29 @@ async function fulfillOneFileWriteRequest(session2, request2) {
|
|
|
111031
111402
|
}
|
|
111032
111403
|
await mkdir10(dirname11(targetResolved.absolutePath), { recursive: true });
|
|
111033
111404
|
await rename4(resolved.absolutePath, targetResolved.absolutePath);
|
|
111034
|
-
await completeWriteRequest(session2,
|
|
111405
|
+
await completeWriteRequest(session2, claimed._id, claimed.revision, { status: "done" });
|
|
111035
111406
|
const elapsed4 = Date.now() - startTime;
|
|
111036
|
-
console.log(`[${formatTimestamp()}] ✏️ File rename fulfilled: ${filePath} → ${
|
|
111407
|
+
console.log(`[${formatTimestamp()}] ✏️ File rename fulfilled: ${filePath} → ${claimed.targetFilePath} (${elapsed4}ms)`);
|
|
111037
111408
|
return;
|
|
111038
111409
|
}
|
|
111039
111410
|
if (operation === "mkdir") {
|
|
111040
111411
|
const exists3 = await fileExistsAt(resolved.absolutePath);
|
|
111041
111412
|
if (exists3) {
|
|
111042
|
-
await completeWriteRequest(session2,
|
|
111413
|
+
await completeWriteRequest(session2, claimed._id, claimed.revision, {
|
|
111043
111414
|
status: "error",
|
|
111044
111415
|
errorMessage: "Directory already exists"
|
|
111045
111416
|
});
|
|
111046
111417
|
return;
|
|
111047
111418
|
}
|
|
111048
111419
|
await mkdir10(resolved.absolutePath, { recursive: true });
|
|
111049
|
-
await completeWriteRequest(session2,
|
|
111420
|
+
await completeWriteRequest(session2, claimed._id, claimed.revision, { status: "done" });
|
|
111050
111421
|
const elapsed4 = Date.now() - startTime;
|
|
111051
111422
|
console.log(`[${formatTimestamp()}] ✏️ Directory mkdir fulfilled: ${filePath} (${elapsed4}ms)`);
|
|
111052
111423
|
return;
|
|
111053
111424
|
}
|
|
111054
111425
|
if (operation === "delete") {
|
|
111055
111426
|
if (filePath === "") {
|
|
111056
|
-
await completeWriteRequest(session2,
|
|
111427
|
+
await completeWriteRequest(session2, claimed._id, claimed.revision, {
|
|
111057
111428
|
status: "error",
|
|
111058
111429
|
errorMessage: "Cannot delete workspace root"
|
|
111059
111430
|
});
|
|
@@ -111061,66 +111432,83 @@ async function fulfillOneFileWriteRequest(session2, request2) {
|
|
|
111061
111432
|
}
|
|
111062
111433
|
const operationCheck2 = await validateWriteOperation(operation, resolved.absolutePath);
|
|
111063
111434
|
if (!operationCheck2.ok) {
|
|
111064
|
-
await completeWriteRequest(session2,
|
|
111435
|
+
await completeWriteRequest(session2, claimed._id, claimed.revision, {
|
|
111065
111436
|
status: "error",
|
|
111066
111437
|
errorMessage: operationCheck2.errorMessage
|
|
111067
111438
|
});
|
|
111068
111439
|
return;
|
|
111069
111440
|
}
|
|
111070
111441
|
await rm4(resolved.absolutePath, { recursive: true, force: false });
|
|
111071
|
-
await completeWriteRequest(session2,
|
|
111442
|
+
await completeWriteRequest(session2, claimed._id, claimed.revision, { status: "done" });
|
|
111072
111443
|
const elapsed4 = Date.now() - startTime;
|
|
111073
111444
|
console.log(`[${formatTimestamp()}] ✏️ File delete fulfilled: ${filePath} (${elapsed4}ms)`);
|
|
111074
111445
|
return;
|
|
111075
111446
|
}
|
|
111076
111447
|
if (operation !== "create" && operation !== "update") {
|
|
111077
|
-
await completeWriteRequest(session2,
|
|
111448
|
+
await completeWriteRequest(session2, claimed._id, claimed.revision, {
|
|
111078
111449
|
status: "error",
|
|
111079
111450
|
errorMessage: unsupportedFileWriteOperationMessage(operation)
|
|
111080
111451
|
});
|
|
111081
111452
|
return;
|
|
111082
111453
|
}
|
|
111083
|
-
|
|
111454
|
+
if (claimed.storageId) {
|
|
111455
|
+
const blockedReason = getBlockedUploadTargetReason(filePath);
|
|
111456
|
+
if (blockedReason) {
|
|
111457
|
+
await completeWriteRequest(session2, claimed._id, claimed.revision, {
|
|
111458
|
+
status: "error",
|
|
111459
|
+
errorMessage: blockedReason
|
|
111460
|
+
});
|
|
111461
|
+
return;
|
|
111462
|
+
}
|
|
111463
|
+
}
|
|
111464
|
+
const payload = await resolveWritePayload(session2, claimed);
|
|
111084
111465
|
if (!payload.ok) {
|
|
111085
|
-
await completeWriteRequest(session2,
|
|
111466
|
+
await completeWriteRequest(session2, claimed._id, claimed.revision, {
|
|
111086
111467
|
status: "error",
|
|
111087
111468
|
errorMessage: payload.errorMessage
|
|
111088
111469
|
});
|
|
111470
|
+
console.warn(`[${formatTimestamp()}] ⚠️ File write failed for ${filePath}: ${payload.errorMessage}`);
|
|
111089
111471
|
return;
|
|
111090
111472
|
}
|
|
111091
111473
|
const operationCheck = await validateWriteOperation(operation, resolved.absolutePath);
|
|
111092
111474
|
if (!operationCheck.ok) {
|
|
111093
|
-
await completeWriteRequest(session2,
|
|
111475
|
+
await completeWriteRequest(session2, claimed._id, claimed.revision, {
|
|
111094
111476
|
status: "error",
|
|
111095
111477
|
errorMessage: operationCheck.errorMessage
|
|
111096
111478
|
});
|
|
111097
111479
|
return;
|
|
111098
111480
|
}
|
|
111099
111481
|
await writePayloadToDisk(resolved.absolutePath, operation, payload.content);
|
|
111100
|
-
await completeWriteRequest(session2,
|
|
111482
|
+
await completeWriteRequest(session2, claimed._id, claimed.revision, { status: "done" });
|
|
111101
111483
|
const elapsed3 = Date.now() - startTime;
|
|
111102
111484
|
console.log(`[${formatTimestamp()}] ✏️ File write fulfilled: ${filePath} (${operation}, ${(payload.content.length / 1024).toFixed(1)}KB, ${elapsed3}ms)`);
|
|
111103
111485
|
} catch (err) {
|
|
111104
111486
|
const message = err instanceof Error ? err.message : "Write failed";
|
|
111105
|
-
|
|
111106
|
-
|
|
111487
|
+
const normalizedMessage = err.code === "EEXIST" ? "File already exists" : message;
|
|
111488
|
+
if (isTerminalFileWriteError(normalizedMessage)) {
|
|
111489
|
+
await completeWriteRequest(session2, claimed._id, claimed.revision, {
|
|
111107
111490
|
status: "error",
|
|
111108
|
-
errorMessage:
|
|
111491
|
+
errorMessage: normalizedMessage
|
|
111109
111492
|
});
|
|
111110
|
-
console.warn(`[${formatTimestamp()}] ⚠️ File write failed for ${filePath}: ${
|
|
111493
|
+
console.warn(`[${formatTimestamp()}] ⚠️ File write failed for ${filePath}: ${normalizedMessage}`);
|
|
111111
111494
|
return;
|
|
111112
111495
|
}
|
|
111113
111496
|
console.warn(`[${formatTimestamp()}] ⚠️ File write transient failure for ${filePath}: ${message} (will retry)`);
|
|
111114
111497
|
}
|
|
111115
111498
|
}
|
|
111116
|
-
var
|
|
111499
|
+
var MAX_INLINE_CONTENT_BYTES, MAX_UPLOAD_CONTENT_BYTES, CHAT_ATTACHMENT_RESERVED_PREFIX = ".chatroom/downloads/attachments/", fulfillFileWriteRequestsEffect;
|
|
111117
111500
|
var init_file_write_fulfillment = __esm(() => {
|
|
111501
|
+
init_chat_attachment_upload_path();
|
|
111502
|
+
init_workspace_upload_path_policy();
|
|
111503
|
+
init_workspace_upload();
|
|
111118
111504
|
init_esm();
|
|
111505
|
+
init_file_write_request_priority();
|
|
111119
111506
|
init_api3();
|
|
111120
111507
|
init_assert_registered_working_dir();
|
|
111121
111508
|
init_workspace_path_security();
|
|
111122
111509
|
init_daemon_services();
|
|
111123
|
-
|
|
111510
|
+
MAX_INLINE_CONTENT_BYTES = 512 * 1024;
|
|
111511
|
+
MAX_UPLOAD_CONTENT_BYTES = MAX_WORKSPACE_UPLOAD_BYTES;
|
|
111124
111512
|
fulfillFileWriteRequestsEffect = exports_Effect.gen(function* () {
|
|
111125
111513
|
const session2 = yield* DaemonSessionService;
|
|
111126
111514
|
const requests = yield* exports_Effect.catchAll(exports_Effect.tryPromise(() => session2.backend.query(api.workspaceFiles.getPendingFileWriteRequests, {
|
|
@@ -111129,8 +111517,9 @@ var init_file_write_fulfillment = __esm(() => {
|
|
|
111129
111517
|
})), () => exports_Effect.succeed([]));
|
|
111130
111518
|
if (requests.length === 0)
|
|
111131
111519
|
return;
|
|
111132
|
-
|
|
111133
|
-
|
|
111520
|
+
const sortedRequests = sortPendingFileWriteRequests(requests);
|
|
111521
|
+
console.log(`[${formatTimestamp()}] ✏️ Received ${sortedRequests.length} pending file write request(s): ${sortedRequests.map((r) => r.filePath).join(", ")}`);
|
|
111522
|
+
for (const request2 of sortedRequests) {
|
|
111134
111523
|
yield* exports_Effect.catchAll(exports_Effect.tryPromise(() => fulfillOneFileWriteRequest(session2, request2)), () => exports_Effect.void);
|
|
111135
111524
|
}
|
|
111136
111525
|
});
|
|
@@ -115842,7 +116231,20 @@ messagesCommand.command("list").description("List messages by sender role or sin
|
|
|
115842
116231
|
});
|
|
115843
116232
|
}
|
|
115844
116233
|
});
|
|
115845
|
-
messagesCommand.command("
|
|
116234
|
+
messagesCommand.command("anchor").description("Locate the last user message and print proof-of-verification workflow commands").requiredOption("--chatroom-id <id>", "Chatroom identifier").requiredOption("--role <role>", "Your role").option("--prior-limit <n>", "Prior user messages to show (default: 3, max: 5)", "3").action(async (options) => {
|
|
116235
|
+
await maybeRequireAuth();
|
|
116236
|
+
let parsedPriorLimit = parseInt(options.priorLimit, 10);
|
|
116237
|
+
if (isNaN(parsedPriorLimit) || parsedPriorLimit < 0)
|
|
116238
|
+
parsedPriorLimit = 3;
|
|
116239
|
+
if (parsedPriorLimit > 5)
|
|
116240
|
+
parsedPriorLimit = 5;
|
|
116241
|
+
const { anchorMessages: anchorMessages2 } = await Promise.resolve().then(() => (init_anchor(), exports_anchor));
|
|
116242
|
+
await anchorMessages2(options.chatroomId, {
|
|
116243
|
+
role: options.role,
|
|
116244
|
+
priorLimit: parsedPriorLimit
|
|
116245
|
+
});
|
|
116246
|
+
});
|
|
116247
|
+
messagesCommand.command("download").description("Download chatroom message history to local files for reading/grep").requiredOption("--chatroom-id <id>", "Chatroom identifier").requiredOption("--role <role>", "Your role").option("--format <format>", "Download format (default: linear)", "linear").option("--output-dir <path>", "Output directory (default: .chatroom/downloads/messages/linear/<download-id>)").option("--limit <n>", "Max messages to download (default: 10, max: 5000)").option("--since-message-id <messageId>", "Download history since this message ID (inclusive, ascending)").action(async (options) => {
|
|
115846
116248
|
await maybeRequireAuth();
|
|
115847
116249
|
if (options.format && options.format !== "linear") {
|
|
115848
116250
|
console.error("❌ Unsupported format. Only --format=linear is supported.");
|
|
@@ -115861,7 +116263,8 @@ messagesCommand.command("download").description("Download chatroom message histo
|
|
|
115861
116263
|
role: options.role,
|
|
115862
116264
|
format: "linear",
|
|
115863
116265
|
outputDir: options.outputDir,
|
|
115864
|
-
limit: parsedLimit
|
|
116266
|
+
limit: parsedLimit,
|
|
116267
|
+
sinceMessageId: options.sinceMessageId
|
|
115865
116268
|
});
|
|
115866
116269
|
});
|
|
115867
116270
|
var contextCommand = program2.command("context").description("Manage chatroom context and state (explicit context management)");
|
|
@@ -116009,4 +116412,4 @@ program2.hook("preAction", async (_thisCommand, actionCommand) => {
|
|
|
116009
116412
|
});
|
|
116010
116413
|
program2.parse();
|
|
116011
116414
|
|
|
116012
|
-
//# debugId=
|
|
116415
|
+
//# debugId=B2378E8C8597995864756E2164756E21
|