@4pm/cli 1.32.0 → 1.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +995 -366
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -28569,6 +28569,365 @@ async function fetchCliVersion(serverUrl, version) {
|
|
|
28569
28569
|
}
|
|
28570
28570
|
}
|
|
28571
28571
|
|
|
28572
|
+
// vendor/@4pm/constants/ai-prompt-defaults.json
|
|
28573
|
+
var ai_prompt_defaults_default = {
|
|
28574
|
+
"server.knowledge.distill": `You are in the working directory of a software project named "{{projectName}}".
|
|
28575
|
+
Read the project (code, docs, config) and write a concise, shareable KNOWLEDGE ARTICLE that
|
|
28576
|
+
distils it for other engineers. Use Markdown and this structure:
|
|
28577
|
+
- a single top-level title line starting with '# '
|
|
28578
|
+
- a one-paragraph summary
|
|
28579
|
+
- '## The problem' \u2014 what problem the project solves
|
|
28580
|
+
- '## The approach' \u2014 the key decisions and how it works
|
|
28581
|
+
- '## What we learned' \u2014 reusable takeaways
|
|
28582
|
+
Output ONLY the Markdown article, no preamble or code fences around the whole thing.{{focusExtra}}`,
|
|
28583
|
+
"cli.research.policy": `You are a research assistant for the 4PM platform. Answer the user's question thoroughly and
|
|
28584
|
+
accurately, in Markdown. Use the read-only web tools (WebSearch / WebFetch) to find current, factual
|
|
28585
|
+
information when helpful, and cite sources inline where relevant.
|
|
28586
|
+
|
|
28587
|
+
STRICT CONTENT POLICY \u2014 you MUST refuse, and must not answer, if the question (or a faithful answer
|
|
28588
|
+
to it) would involve any of:
|
|
28589
|
+
- sexual, abusive, hateful, or illegal content;
|
|
28590
|
+
- political persuasion, campaigning, or partisan advocacy;
|
|
28591
|
+
- discrimination or demeaning content targeting a race, ethnicity, religion, gender, nationality, or
|
|
28592
|
+
other protected group;
|
|
28593
|
+
- probing, scanning, exploiting, or attacking the security of ANY system, network, account, device,
|
|
28594
|
+
or person you do not own (no vulnerability hunting, no exploit/malware creation, no credential or
|
|
28595
|
+
PII harvesting, no evasion of security controls). General defensive-security education is allowed.
|
|
28596
|
+
- any other seriously harmful activity.
|
|
28597
|
+
|
|
28598
|
+
When you must refuse, reply with EXACTLY one line starting with "REFUSED:" followed by a short,
|
|
28599
|
+
neutral reason, and output nothing else.
|
|
28600
|
+
|
|
28601
|
+
You have READ-ONLY web tools only. Never attempt to read local files, run shell commands, or interact
|
|
28602
|
+
with other systems.
|
|
28603
|
+
|
|
28604
|
+
---
|
|
28605
|
+
|
|
28606
|
+
Question:
|
|
28607
|
+
{{question}}`,
|
|
28608
|
+
"cli.memory.compact": "You maintain a COMPACT running memory of an ongoing assistant conversation so it can continue\nacross sessions and accounts. Merge the latest exchange into the existing memory and return the\nUPDATED memory. Keep it UNDER {{budgetChars}} characters. Use terse Markdown bullets grouped\nas: Decisions, Constraints, Open tasks, Glossary. Keep durable facts; drop chit-chat and anything\nnow obsolete. Output ONLY the updated memory text \u2014 no preamble, no code fences.\n\n=== EXISTING MEMORY ===\n{{oldMemory}}\n\n=== LATEST USER PROMPT ===\n{{prompt}}\n\n=== LATEST ASSISTANT ANSWER ===\n{{answer}}",
|
|
28609
|
+
"cli.support.answer": `You are the 4PM product support assistant. Answer the user's question about how to use 4PM
|
|
28610
|
+
using ONLY the documentation and FAQ provided below. If the answer is not in the docs, say
|
|
28611
|
+
you don't have that information and suggest contacting human support \u2014 do not invent details.
|
|
28612
|
+
{{roleNote}}
|
|
28613
|
+
Answer concisely in the user's language; reference the relevant doc heading/path when useful.
|
|
28614
|
+
|
|
28615
|
+
Return ONLY a single JSON object (no markdown fences, no prose around it) with these keys:
|
|
28616
|
+
"onTopic": boolean \u2014 false if the question is NOT about how to use the 4PM product,
|
|
28617
|
+
"sensitive": boolean \u2014 true if the question is inappropriate / abusive OR asks how to hack,
|
|
28618
|
+
bypass security, gain unauthorized access, take over or steal another
|
|
28619
|
+
user's/org's account or data, obtain credentials, or any malicious intent,
|
|
28620
|
+
"reason": string \u2014 a short reason when onTopic is false or sensitive is true, else "",
|
|
28621
|
+
"answer": string \u2014 your reply to the user, in markdown (in the user's language).
|
|
28622
|
+
Refusal policy (ADR-0237): if the question is off-topic (onTopic false) OR sensitive, DO NOT
|
|
28623
|
+
answer it. Set "answer" to this EXACT same polite refusal for BOTH cases (in the user's
|
|
28624
|
+
language, do not reveal which category or hint at the reason): "Sorry, I can only help with
|
|
28625
|
+
questions about how to use 4PM. For anything else, please contact human support." Only answer
|
|
28626
|
+
normally when onTopic is true AND sensitive is false.
|
|
28627
|
+
|
|
28628
|
+
===== DOCUMENTATION =====
|
|
28629
|
+
{{docs}}
|
|
28630
|
+
|
|
28631
|
+
===== QUESTION =====
|
|
28632
|
+
{{question}}`,
|
|
28633
|
+
"cli.support.reply_draft": "You are drafting a reply FROM the 4PM support team TO the customer in the support ticket below.\nAddress the customer's latest message, using the whole conversation for context. Be accurate,\nfriendly and concise. Write in the language the customer uses in the ticket.\nUse the documentation and FAQ below for any product facts; never invent features, prices or\ncommitments that are not in the docs \u2014 leave a clear [placeholder] for the admin to fill instead.\nFollow the admin's instructions when given. A human admin reviews and edits your draft before\nsending it.\n\nOutput ONLY the message body in markdown \u2014 no subject line, no preamble, no explanation, no code\nfences around the whole message.\n\n===== DOCUMENTATION =====\n{{docs}}\n\n===== CONTEXT =====\n{{context}}",
|
|
28634
|
+
"cli.support.outreach_draft": "You are drafting an outreach message FROM the 4PM platform team TO the customer organizations.\nUse the subject and any current draft below as the starting point. Be clear, friendly and concise.\nWrite in the language of the subject/draft (or of the admin's instructions when those are empty).\nUse the documentation and FAQ below for any product facts; never invent features, prices or\ncommitments that are not in the docs \u2014 leave a clear [placeholder] for the admin to fill instead.\nFollow the admin's instructions when given. A human admin reviews and edits your draft before\nsending it.\n\nOutput ONLY the message body in markdown \u2014 no subject line, no preamble, no explanation, no code\nfences around the whole message.\n\n===== DOCUMENTATION =====\n{{docs}}\n\n===== CONTEXT =====\n{{context}}",
|
|
28635
|
+
"cli.support.legal_draft": "You are drafting the body of one of 4PM's legal documents (Terms of Service, Privacy Policy,\nAdd-on Terms, Rented Machine Addendum, \u2026) for the platform admin to review and publish.\nWrite or revise the WHOLE document body in the target locale given in the context. Keep the\nexisting heading structure and numbering unless the admin's instructions say otherwise.\nWhen the target locale is not English and an English reference is given, follow its meaning closely.\nRules:\n- Every {{placeholder}} (e.g. {{companyName}}) must be kept verbatim \u2014 never fill or rename it.\n- Describe plans, prices, upgrades, downgrades, cancellations, renewals, refunds and add-ons ONLY\n as stated in the PLAN CATALOG and BILLING FACTS below or the documentation; never invent\n commitments, prices or legal guarantees. Where something is unknown write `[TODO: \u2026]`.\n- Plain, precise legal English (or the target language); short numbered sections and bullet lists.\n- This is a draft, not legal advice; do not add a disclaimer about that inside the document.\n\nOutput ONLY the document body in markdown \u2014 no title line, no preamble, no explanation, no code\nfences around the whole document.\n\n===== DOCUMENTATION =====\n{{docs}}\n\n===== CONTEXT =====\n{{context}}",
|
|
28636
|
+
"cli.faq.compose": "You are maintaining the 4PM product FAQ, stored as markdown in THIS git repository (your CWD).\nFrom the resolved support tickets below, distil reusable, product-general FAQ entries and add or\nupdate the appropriate markdown file(s) in this repo (e.g. an FAQ.md, or the closest existing\ntopic file \u2014 inspect the repo first and match its structure/style).\n\nRules:\n- Write generalized Q&A, NOT ticket-specific replies; never include customer names, emails, ids,\n org names or any personal data from the tickets.\n- Merge with existing entries instead of duplicating; keep the existing formatting and headings.\n- If a ticket yields no reusable FAQ value, skip it. If nothing is worth adding, make NO changes.\n- Only edit markdown documentation files; do not touch code, CI, or unrelated files.\n- Do NOT run git commit/push or open a PR yourself \u2014 just leave the edited files in the working\n tree; the surrounding tooling handles commit + PR.{{customInstruction}}\n\n===== RESOLVED TICKETS =====\n{{transcript}}\n\n===== REQUIRED FINAL OUTPUT =====\nAfter finishing all file edits, output \u2014 as the LAST thing, on its own \u2014 a single fenced JSON\nblock summarizing what you did PER TICKET (created/updated which FAQ entry, or skipped + why):\n```json\n{{ticketJsonSkeleton}}\n```\nInclude exactly one entry per ticket id above; keep each summary to one or two sentences.",
|
|
28637
|
+
"cli.scaffold.readme": "Write a concise README.md (Markdown only, no preamble) for this project from its spec JSON:\n{{specJson}}",
|
|
28638
|
+
"cli.scaffold.guide": "Write a {{guideFile}} (Markdown only, no preamble) with guidance/conventions for AI agents working in this project, derived from its spec JSON:\n{{specJson}}{{guideInstructionsBlock}}",
|
|
28639
|
+
"cli.autonomous.intake": "You are the 4PM autonomous INTAKE step (headless). Edit ONLY `USER_TODO.md`, `USER_QA.md` and\n`AI_TODO.md`. Do NOT run git, do NOT touch `AI_PROGRESS.md` / `AI_DONE.md` / any code \u2014 the 4PM cli\ncommits and publishes your book edits. Write in the language the project's `CLAUDE.md` specifies (default English).\n\n{{booksIntro}}\n\n## 1 \u2014 Approved user requests \u2192 tasks\nRead `USER_TODO.md` (columns `ID | Group | Depends | Request`). For each APPROVED `REQ-\u2026` whose `Depends`\nare already handled:\n- CLEAR request \u21D2 split it into tasks appended to `AI_TODO.md` (columns\n `ID | Priority | Tag | Depends | Group | Task description | Notes`), ids `TSK-{groupid:0000}-{taskid:0000}`\n (groupid UNIQUE + INCREASING across all history \u2014 the largest ever used in `AI_TODO.md`/`AI_DONE.md`/\n `AI_PROGRESS.md`/their git history, +1; taskid from 0001). `Depends` = the `TSK-\u2026` it needs.\n Remove the analysed request row (empty \u21D2 reset to the empty template).\n- UNCLEAR request \u21D2 do NOT guess: append a `USER_QA.md` row (`ID | Group | Depends | Original request |\n Question / options | Answer`) with a new `QA-{groupid:0000}-{qaid:0000}`, blank `Answer`; remove the request row.\nAn unapproved request stays.\n\n## 2 \u2014 Answered intake questions\nFor each `USER_QA.md` row with an `Answer`, APPROVED, and whose id is NOT in any `AI_TODO.md` `Depends`\n(those are task questions \u2014 the cli handles them; leave them alone): re-analyse the original request with the\nanswer into tasks (same rules), then remove that QA row.\n\n## Task size (MANDATORY \u2014 every task must be finishable in ONE run)\nSize each task by SCOPE and write `size: S` or `size: M` at the start of `Notes`:\n- **S** \u2014 one small change in one module/layer (\u2248 \u2264 {{sMaxFiles}} files, \u2248 \u2264 {{sMaxLines}} lines), e.g. fix a bug, add a field.\n- **M** \u2014 one feature slice across 2\u20133 layers with ONE purpose (\u2248 \u2264 {{mMaxFiles}} files, \u2248 \u2264 {{mMaxLines}} lines), e.g. one CRUD screen, one API + its UI + tests.\n- **L** \u2014 several features, a cross-cutting change, a big migration, a new subsystem, or an architecture decision.\n **Never write an L task**: split it into S/M tasks chained with `Depends`, or ask in `USER_QA.md` when the split needs a decision.\nThe counts are hints, not hard limits. Each task's `Task description` states the acceptance criteria and how to test it.\n\nWhen done, reply with one short line summarising what you added.",
|
|
28640
|
+
"cli.autonomous.implement": 'You are the 4PM autonomous IMPLEMENT step (headless). Implement exactly ONE task, then STOP.\n\nTask {{taskId}} (group {{group}}, attempt {{attempt}}):\n{{desc}}\nNotes: {{notes}}\n{{resumedNote}}\nBranches (already checked out by the 4PM cli \u2014 stay on them):\n- project root is on `{{branch}}`\n{{subLines}}\n\nRules:\n- Follow the project\'s architecture and `CLAUDE.md`. Add/update tests; run the project\'s test command and WAIT for the result.\n- Commit your work (`git add` + `git commit`) in the root and/or inside each submodule you changed.\n- Do NOT push, do NOT open pull requests, do NOT switch/create branches, do NOT edit the book files\n (USER_TODO/USER_QA/AI_TODO/AI_PROGRESS/AI_DONE) \u2014 the 4PM cli delivers and records everything.\n- If you need a human DECISION to continue (ambiguous requirement, missing information, a choice with real\n trade-offs), do NOT guess: commit what you have and report `needs-input` with the question + options.\n\nFinish with ONLY one fenced `json` block:\n{"status": "done" | "needs-input" | "failed", "summary": "<what you did / why it failed>", "question": "<only for needs-input: the question and the options>"}',
|
|
28641
|
+
"cli.autonomous.split": 'You are the 4PM autonomous SPLIT step (read-only \u2014 do not change any file). Task {{taskId}} failed to\nfinish after repeated attempts, so it is probably too big for one run. Split it into 2\u20136 smaller child tasks\nthat are each finishable in ONE run (size S or M), in execution order (each depends on the previous).\n\nTask: {{desc}}\nNotes: {{notes}}\nWhy the attempts ended: {{reasons}}\nWork already done on its WIP branch `{{branch}}` (diff against the base, may be truncated):\n```\n{{diff}}\n```\nThe first child continues from that WIP branch: note which parts are already done so they are not redone.\nEach child description must state its acceptance criteria and how to test it.\n\nReply with ONLY one fenced `json` block:\n{"children": [{"desc": "<task + acceptance criteria>", "priority": "High|Medium|Low", "size": "S|M", "notes": "<optional>"}]}',
|
|
28642
|
+
"web.generators": "You are producing {{what}} for a software project, derived strictly from its specification (the source of truth). Evaluate (when relevant): business logic, project security, AI-usage security, technical soundness, methodology/approach, and overall feasibility & coherence. Write a clear, well-structured **Markdown** document. Output ONLY the Markdown document \u2014 no preamble and no wrapping code fence.{{templateHint}}{{paramsBlock}}\nProject spec \u2014 self-describing JSON (fields with id/name/description/type/options/value):\n{{specContext}}",
|
|
28643
|
+
"web.guide": 'You are producing a {{file}} file \u2014 guidance and conventions for AI coding agents working in this project \u2014 derived strictly from its specification (the source of truth). Evaluate (when relevant): business logic, project security, AI-usage security, technical soundness, methodology/approach, and overall feasibility & coherence. Write a clear, well-structured **Markdown** document. Output ONLY the Markdown document \u2014 no preamble and no wrapping code fence.\nThe document MUST include a prominent, mandatory section near the top (e.g. "## Security") that instructs the agent to READ and STRICTLY FOLLOW the project\'s security policy file `AI_SECURITY.md` (at the repository root) before doing any work, and that its rules \u2014 secret handling, data masking, and commit rules \u2014 override anything else when they conflict.{{baseBlock}}{{extraBlock}}\nProject spec \u2014 self-describing JSON (fields with id/name/description/type/options/value):\n{{specContext}}',
|
|
28644
|
+
"web.draft_tasks": 'You are a senior software project manager. From the project specification below (the source of truth), produce a concise, well-ordered list of concrete, actionable engineering tasks to build this project. Prefer vertical slices, keep each task independently shippable, and order by dependency. Output ONLY a JSON array \u2014 no preamble, no code fence. Each element is an object with exactly these string fields:\n- "priority": one of "High", "Medium", "Low"\n- "group": a short phase/area label (e.g. "Setup", "Auth", "API", "UI")\n- "depends": comma-separated titles of tasks in THIS list it depends on (or "")\n- "description": the task as a clear imperative sentence\n- "notes": optional extra detail (or "")\n\nProject spec \u2014 self-describing JSON (fields with id/name/description/type/options/value):\n{{specContext}}',
|
|
28645
|
+
"web.spec.review": 'Review this software project specification WITHOUT changing it \u2014 you only advise. Evaluate (when relevant): business logic, project security, AI-usage security, technical soundness, methodology/approach, and overall feasibility & coherence. Reply in EXACTLY this format:\nASSESSMENT:\n<a short overall assessment of feasibility, coherence and reasonableness, in Markdown>\nISSUES:\n<one point that is unclear, inconsistent or unreasonable per line, or "none">\nDo not rewrite or output the spec \u2014 reply with only the assessment and issues. Spec \u2014 self-describing JSON (fields with id/name/description/type/options/value):\n{{specContext}}',
|
|
28646
|
+
"web.spec.compose": `You are revising a software project specification for consistency and feasibility, guided by the review in its "meta._aiReview" field. Make MINIMAL edits: keep the author's original intent and wording, only tighten coherence, fill clear gaps and link the fields together sensibly. Do NOT rewrite drastically or add scope the author did not intend. Evaluate (when relevant): business logic, project security, AI-usage security, technical soundness, methodology/approach, and overall feasibility & coherence. Then judge whether the revised spec has enough coherent information to create the project. Reply with ONLY a single-line minified JSON object (no preamble, no code fence, no pretty-printing/indentation) of the form {"changes": [{"field": "<id>", "before": "<old value>", "after": "<new value>"}], "tree": "<source directory tree>", "ok": <true|false>, "assessment": "<one short Markdown paragraph on the result>", "blockers": ["<one must-fix gap per item; empty when ok>"]}. Include a "changes" entry ONLY for each field you actually edit \u2014 do NOT echo unchanged fields and do NOT return the whole spec. Use the SAME field ids as the input; "before"/"after" carry that field's value in its native JSON type (a string for text/select fields, a JSON array of strings for multiselect fields) and MUST keep select/multiselect values within their allowed options. Set "ok" true ONLY when nothing blocks creating the project. The "tree" is a plain-text ASCII directory tree for the project's source layout, derived from the chosen architecture, source pattern, tech stack and modules (use \u251C\u2500 \u2502 \u2514\u2500 connectors; a trailing "/" marks folders). Current spec \u2014 self-describing JSON (fields with id/name/description/type/options/value):
|
|
28647
|
+
{{specContext}}`,
|
|
28648
|
+
"web.spec.suggest": "You are helping fill a software project specification. Suggest a value for {{target}} Evaluate (when relevant): business logic, project security, AI-usage security, technical soundness, methodology/approach, and overall feasibility & coherence. {{seed}}{{shape}} {{instructionBlock}}Context \u2014 the full spec so far as a self-describing JSON (each field has id/name/description/type/options/value; use it to stay coherent):\n{{specContext}}",
|
|
28649
|
+
"web.spec.group_suggest": `You are helping fill a software project specification. Suggest values for ALL fields of the "{{groupKey}}" section below, in one pass, keeping them coherent with each other and with the rest of the spec. Evaluate (when relevant): business logic, project security, AI-usage security, technical soundness, methodology/approach, and overall feasibility & coherence. For each field: if it already has a non-empty value, treat it as the author's intent \u2014 keep it and only refine/expand; otherwise fill it. For select fields return exactly one of the allowed options (or "" if none fits); for multiselect return an array of allowed options; for textarea return detailed clean Markdown; for text return a concise single-line string. Fields:
|
|
28650
|
+
{{fieldLines}}
|
|
28651
|
+
{{instructionBlock}}Reply with ONLY a single-line minified JSON object mapping each field id to its value \u2014 no preamble, no code fence, no markdown, no extra keys, no pretty-printing/indentation. Context \u2014 the spec as self-describing JSON:
|
|
28652
|
+
{{specContext}}`,
|
|
28653
|
+
"web.spec.subagents_suggest": 'You are helping declare the custom AI subagents of a software project. Each subagent becomes a ".claude/agents/<name>.md" file the coding assistant can delegate to. Propose a small, focused set (typically 2\u20135) of subagents that fit THIS project, coherent with its architecture, tech stack and modules. Evaluate (when relevant): business logic, project security, AI-usage security, technical soundness, methodology/approach, and overall feasibility & coherence. Keep every already-declared subagent (refine its description if useful) and add the missing ones. Each subagent has a "name" (a short kebab-case identifier, e.g. "code-reviewer") and a "description" (one or two sentences on its role and when to use it). Already declared: {{current}}. {{instructionBlock}}Reply with ONLY a single-line minified JSON array of {"name","description"} objects \u2014 no preamble, no code fence, no markdown, no extra keys, no pretty-printing/indentation. Context \u2014 the spec as self-describing JSON:\n{{specContext}}',
|
|
28654
|
+
"web.spec.subagent_desc": `You are helping declare the custom AI subagents of a software project. Each subagent becomes a ".claude/agents/<name>.md" file the coding assistant can delegate to. Write the description for {{who}}: one or two sentences on its role and when to use it, specific to THIS project's architecture, tech stack and modules. Evaluate (when relevant): business logic, project security, AI-usage security, technical soundness, methodology/approach, and overall feasibility & coherence. {{seed}}{{others}}{{instructionBlock}}Reply with ONLY the description text \u2014 no preamble, no code fence, no name, no JSON, no quotes. Context \u2014 the spec as self-describing JSON:
|
|
28655
|
+
{{specContext}}`,
|
|
28656
|
+
"web.template.analyze": "You are assessing a 4PM project-template (scaffold) update WITHOUT changing anything \u2014 advisory only. This project was scaffolded from template version {{localVersion}}; the latest is {{latestVersion}}. The template changelog for the versions this project would gain:\n{{changelog}}\n\nInspect the project's current files (they may have been customized since scaffolding) and report, in clear Markdown:\n1. What the update changes (config, hooks, conventions, layout).\n2. Which of THIS project's files are affected.\n3. Likely conflicts with local customizations (call out anything risky to overwrite).\n4. Overall effort and risk.\nDo NOT edit, create, delete, or move any file, and do NOT run git write commands \u2014 output only the report.{{extras}}",
|
|
28657
|
+
"web.template.update": 'You are applying a 4PM project-template (scaffold) update as a REVIEWED change \u2014 never a blind overwrite, and never a merge into the main branch. This project was scaffolded from template version {{localVersion}}; update it to {{latestVersion}}. The template changelog for the versions to apply:\n{{changelog}}\n\nSteps (do them in order; stop and report if one cannot be completed):\n{{resolveBase}}1. Run "git fetch origin", then create and switch to a new git branch "{{branch}}" FROM "origin/{{baseRef}}" (never from whatever branch is currently checked out): git checkout -B "{{branch}}" "origin/{{baseRef}}".\n2. Apply the template changes above to this project. The files may have been customized \u2014 MERGE the changes, preserving local customizations; never clobber intentional edits. Add new template files/hooks/config as needed.\n3. Update (or create) "./.4pm/.4pm.json" so its "templateVersion" is "{{latestVersion}}", keeping any other fields.\n4. Commit everything on "{{branch}}" with a clear message, then PUSH the branch to the remote.\n5. Open a PULL REQUEST from "{{branch}}" INTO "{{baseRef}}" using the host CLI that matches the remote \u2014 "gh pr create --base {{baseRef}} --head {{branch}}" for GitHub, "glab mr create --target-branch {{baseRef}} --source-branch {{branch}}" for GitLab (do NOT merge it). Capture the created PR/MR number and its URL.\nDo NOT stop to ask for confirmation \u2014 you are authorized to run these git and PR commands.{{extras}}\n\nWhen finished, reply with ONLY a single fenced code block tagged json (no prose before or after) of the form {"status": "success" | "blocked" | "failed", "pullRequestId": "<the PR/MR number, or null>", "pullRequestUrl": "<the PR/MR URL, or null>", "branch": "<the branch you created, e.g. {{branch}}>", "message": "<one short line: what changed on success, or WHY it could not complete>"}. Use "success" ONLY if the pull request was actually created and you have its id/number; if you were blocked (e.g. a command needed approval, or "gh"/"glab" is missing or not authenticated) use "blocked"; use "failed" for an outright error. Never report "success" without a real pull request id.',
|
|
28658
|
+
"web.checklist.eval": `You are checking a checklist against the {{contextLabel}} of this project. You MAY read the project's files (read-only) but MUST NOT write anything.
|
|
28659
|
+
|
|
28660
|
+
Checklist "{{name}}" \u2014 decide a verdict for EACH item: pass, fail, skip, or na.
|
|
28661
|
+
Items:
|
|
28662
|
+
{{lines}}
|
|
28663
|
+
|
|
28664
|
+
{{contextLabel}}:
|
|
28665
|
+
{{contextText}}
|
|
28666
|
+
|
|
28667
|
+
Reply with EXACTLY one line per item, in the form:
|
|
28668
|
+
[<ID>] <pass|fail|skip|na> \u2014 <one-line reason>
|
|
28669
|
+
No other text, no code fences.`,
|
|
28670
|
+
"web.checklist.author": `You are helping author a reusable project checklist (a "definition of ready"-style rubric). Produce the checklist's items based on the author's instruction, keeping the result coherent and actionable. Treat the current items as the author's intent: keep the ones that should stay, refine wording where it helps, and add the missing ones.
|
|
28671
|
+
Current items (JSON): {{current}}
|
|
28672
|
+
Author's instruction:
|
|
28673
|
+
"""
|
|
28674
|
+
{{instruction}}
|
|
28675
|
+
"""
|
|
28676
|
+
Reply with ONLY a single-line minified JSON array of the FULL new item list \u2014 each element an object with "text" (string, required), optional "description" (string) and optional "required" (boolean). No ids, no preamble, no code fence, no pretty-printing.`,
|
|
28677
|
+
"web.memo.compose": `You are turning a set of a teammate's project notes into ONE community forum post for the team. The notes may reference pasted screenshots/diagrams as [Image#N] tokens \u2014 for each, use the Read tool on the file path the token was rewritten to, look at the image, and weave what it shows into the post where relevant. Write a clear, helpful, self-contained post (not a raw copy of the notes). Reply with ONLY a single fenced code block tagged json containing an object of the form {"title": "<a short title, at most 200 characters>", "body": "<the post body as plain text, at most 10000 characters>"} \u2014 no prose before or after the fence.
|
|
28678
|
+
|
|
28679
|
+
Notes:
|
|
28680
|
+
{{notes}}`,
|
|
28681
|
+
"web.verify": "You are verifying entries in this project's {{verifyBook}} book. You MAY read the project's files (read-only) but MUST NOT write anything.\n\nSelected entries (use all of them together as shared context, then answer each one):\n{{entries}}\n\nQuestion to answer for EACH entry: {{question}}\n\nReply with EXACTLY one line per entry, in the form:\n[<ID>] <concise one-line answer>\nNo other text, no code fences.",
|
|
28682
|
+
"web.git.resolve": "Resolve all git merge conflicts in {{repoScope}} of the current project. Edit each conflicted file to a correct merged result (remove all conflict markers), then run `git add` on each. Do NOT commit \u2014 leave the merge staged for review."
|
|
28683
|
+
};
|
|
28684
|
+
|
|
28685
|
+
// vendor/@4pm/constants/ai-prompts.ts
|
|
28686
|
+
var DEFAULTS = ai_prompt_defaults_default;
|
|
28687
|
+
function renderPrompt(template, vars) {
|
|
28688
|
+
return template.replace(
|
|
28689
|
+
/\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g,
|
|
28690
|
+
(match, name) => Object.prototype.hasOwnProperty.call(vars, name) ? String(vars[name]) : match
|
|
28691
|
+
);
|
|
28692
|
+
}
|
|
28693
|
+
var PROMPT_META = [
|
|
28694
|
+
{
|
|
28695
|
+
key: "server.knowledge.distill",
|
|
28696
|
+
ownerApp: "server",
|
|
28697
|
+
phase: 1,
|
|
28698
|
+
title: "Knowledge distillation",
|
|
28699
|
+
description: "Turns a project into a shareable knowledge article (read-only agent in the project folder).",
|
|
28700
|
+
requiredVars: ["projectName"]
|
|
28701
|
+
},
|
|
28702
|
+
{
|
|
28703
|
+
key: "cli.research.policy",
|
|
28704
|
+
ownerApp: "cli",
|
|
28705
|
+
phase: 1,
|
|
28706
|
+
title: "AI Research \u2014 system policy",
|
|
28707
|
+
description: "System instruction + content policy prepended to an org Research question.",
|
|
28708
|
+
requiredVars: ["question"]
|
|
28709
|
+
},
|
|
28710
|
+
{
|
|
28711
|
+
key: "cli.memory.compact",
|
|
28712
|
+
ownerApp: "cli",
|
|
28713
|
+
phase: 1,
|
|
28714
|
+
title: "Console memory compaction",
|
|
28715
|
+
description: "Merges the latest exchange into the rolling per-project AI memory, budget-bounded.",
|
|
28716
|
+
requiredVars: ["budgetChars", "oldMemory", "prompt", "answer"]
|
|
28717
|
+
},
|
|
28718
|
+
{
|
|
28719
|
+
key: "cli.support.answer",
|
|
28720
|
+
ownerApp: "cli",
|
|
28721
|
+
phase: 1,
|
|
28722
|
+
title: "AI Help \u2014 support answer",
|
|
28723
|
+
description: "Answers a user/admin product question from the KB docs, with the inline moderation JSON contract. Edit with care: the response must stay a single JSON object with onTopic/sensitive/reason/answer.",
|
|
28724
|
+
requiredVars: ["roleNote", "docs", "question"]
|
|
28725
|
+
},
|
|
28726
|
+
{
|
|
28727
|
+
key: "cli.support.reply_draft",
|
|
28728
|
+
ownerApp: "cli",
|
|
28729
|
+
phase: 1,
|
|
28730
|
+
title: "Admin draft \u2014 ticket reply",
|
|
28731
|
+
description: "Drafts a support-team reply to a ticket (freeform markdown).",
|
|
28732
|
+
requiredVars: ["docs", "context"]
|
|
28733
|
+
},
|
|
28734
|
+
{
|
|
28735
|
+
key: "cli.support.outreach_draft",
|
|
28736
|
+
ownerApp: "cli",
|
|
28737
|
+
phase: 1,
|
|
28738
|
+
title: "Admin draft \u2014 outreach message",
|
|
28739
|
+
description: "Drafts an outreach message to customer orgs (freeform markdown).",
|
|
28740
|
+
requiredVars: ["docs", "context"]
|
|
28741
|
+
},
|
|
28742
|
+
{
|
|
28743
|
+
key: "cli.support.legal_draft",
|
|
28744
|
+
ownerApp: "cli",
|
|
28745
|
+
phase: 1,
|
|
28746
|
+
title: "Admin draft \u2014 legal document body",
|
|
28747
|
+
description: "Drafts/revises a legal document body in a locale, keeping legal {{placeholders}} verbatim.",
|
|
28748
|
+
requiredVars: ["docs", "context"]
|
|
28749
|
+
},
|
|
28750
|
+
{
|
|
28751
|
+
key: "cli.faq.compose",
|
|
28752
|
+
ownerApp: "cli",
|
|
28753
|
+
phase: 1,
|
|
28754
|
+
title: "Ticket \u2192 FAQ synthesis",
|
|
28755
|
+
description: "Agent-write run that distils resolved tickets into FAQ markdown + a per-ticket JSON summary. Keep the required JSON output block and the {{ticketJsonSkeleton}} intact.",
|
|
28756
|
+
requiredVars: ["transcript", "ticketJsonSkeleton"]
|
|
28757
|
+
},
|
|
28758
|
+
{
|
|
28759
|
+
key: "cli.scaffold.readme",
|
|
28760
|
+
ownerApp: "cli",
|
|
28761
|
+
phase: 1,
|
|
28762
|
+
title: "Scaffold \u2014 README author",
|
|
28763
|
+
description: "Writes the initial README.md for a new project from its spec JSON (on project create).",
|
|
28764
|
+
requiredVars: ["specJson"]
|
|
28765
|
+
},
|
|
28766
|
+
{
|
|
28767
|
+
key: "cli.scaffold.guide",
|
|
28768
|
+
ownerApp: "cli",
|
|
28769
|
+
phase: 1,
|
|
28770
|
+
title: "Scaffold \u2014 agent guide author",
|
|
28771
|
+
description: "Writes the project's CLAUDE.md / AGENT.md guide from its spec JSON (on project create).",
|
|
28772
|
+
requiredVars: ["guideFile", "specJson"]
|
|
28773
|
+
},
|
|
28774
|
+
{
|
|
28775
|
+
key: "cli.autonomous.intake",
|
|
28776
|
+
ownerApp: "cli",
|
|
28777
|
+
phase: 1,
|
|
28778
|
+
title: "Autonomous \u2014 intake",
|
|
28779
|
+
description: "Analyses approved user requests + answered intake questions into sized AI tasks (books only; no git/code).",
|
|
28780
|
+
requiredVars: ["booksIntro", "sMaxFiles", "sMaxLines", "mMaxFiles", "mMaxLines"]
|
|
28781
|
+
},
|
|
28782
|
+
{
|
|
28783
|
+
key: "cli.autonomous.implement",
|
|
28784
|
+
ownerApp: "cli",
|
|
28785
|
+
phase: 1,
|
|
28786
|
+
title: "Autonomous \u2014 implement",
|
|
28787
|
+
description: "Implements exactly one task on the prepared branches (commit only). Keep the final JSON status block intact.",
|
|
28788
|
+
requiredVars: ["taskId", "desc", "branch", "subLines"]
|
|
28789
|
+
},
|
|
28790
|
+
{
|
|
28791
|
+
key: "cli.autonomous.split",
|
|
28792
|
+
ownerApp: "cli",
|
|
28793
|
+
phase: 1,
|
|
28794
|
+
title: "Autonomous \u2014 split",
|
|
28795
|
+
description: "Read-only: proposes smaller child tasks when a task repeatedly fails. Keep the final JSON children block intact.",
|
|
28796
|
+
requiredVars: ["taskId", "desc", "reasons", "branch", "diff"]
|
|
28797
|
+
},
|
|
28798
|
+
{
|
|
28799
|
+
key: "web.generators",
|
|
28800
|
+
ownerApp: "web",
|
|
28801
|
+
phase: 2,
|
|
28802
|
+
title: "Content generator",
|
|
28803
|
+
description: "Produces a report / estimation / WBS / proposal / feasibility Markdown document from the project spec.",
|
|
28804
|
+
requiredVars: ["what", "specContext"]
|
|
28805
|
+
},
|
|
28806
|
+
{
|
|
28807
|
+
key: "web.guide",
|
|
28808
|
+
ownerApp: "web",
|
|
28809
|
+
phase: 2,
|
|
28810
|
+
title: "AI guide (CLAUDE.md / AGENT.md)",
|
|
28811
|
+
description: "Writes/refines the project's agent-guide file from the spec. Keep the mandatory AI_SECURITY.md section.",
|
|
28812
|
+
requiredVars: ["file", "specContext"]
|
|
28813
|
+
},
|
|
28814
|
+
{
|
|
28815
|
+
key: "web.draft_tasks",
|
|
28816
|
+
ownerApp: "web",
|
|
28817
|
+
phase: 2,
|
|
28818
|
+
title: "Draft tasks from spec",
|
|
28819
|
+
description: "Breaks the spec into a JSON array of engineering tasks. Keep the required JSON array output shape.",
|
|
28820
|
+
requiredVars: ["specContext"]
|
|
28821
|
+
},
|
|
28822
|
+
{
|
|
28823
|
+
key: "web.spec.review",
|
|
28824
|
+
ownerApp: "web",
|
|
28825
|
+
phase: 2,
|
|
28826
|
+
title: "Spec review (advisory)",
|
|
28827
|
+
description: "Advises on the whole spec in the fixed ASSESSMENT/ISSUES format (no rewrite). Keep that reply format.",
|
|
28828
|
+
requiredVars: ["specContext"]
|
|
28829
|
+
},
|
|
28830
|
+
{
|
|
28831
|
+
key: "web.spec.compose",
|
|
28832
|
+
ownerApp: "web",
|
|
28833
|
+
phase: 2,
|
|
28834
|
+
title: "Spec compose (gates create)",
|
|
28835
|
+
description: "Revises the spec with minimal edits + judges readiness. GATES project creation \u2014 keep the exact JSON object shape (changes/tree/ok/assessment/blockers).",
|
|
28836
|
+
requiredVars: ["specContext"]
|
|
28837
|
+
},
|
|
28838
|
+
{
|
|
28839
|
+
key: "web.spec.suggest",
|
|
28840
|
+
ownerApp: "web",
|
|
28841
|
+
phase: 2,
|
|
28842
|
+
title: "Spec field suggest (\u2728)",
|
|
28843
|
+
description: "Suggests a value for one spec field. {{shape}} carries the reply-format rule the parser depends on.",
|
|
28844
|
+
requiredVars: ["target", "shape", "specContext"]
|
|
28845
|
+
},
|
|
28846
|
+
{
|
|
28847
|
+
key: "web.spec.group_suggest",
|
|
28848
|
+
ownerApp: "web",
|
|
28849
|
+
phase: 2,
|
|
28850
|
+
title: "Spec section suggest",
|
|
28851
|
+
description: "Suggests values for a whole wizard section. Keep the single-line JSON object (field id \u2192 value) output shape.",
|
|
28852
|
+
requiredVars: ["groupKey", "fieldLines", "specContext"]
|
|
28853
|
+
},
|
|
28854
|
+
{
|
|
28855
|
+
key: "web.spec.subagents_suggest",
|
|
28856
|
+
ownerApp: "web",
|
|
28857
|
+
phase: 2,
|
|
28858
|
+
title: "Subagents suggest",
|
|
28859
|
+
description: "Proposes the project's AI subagents. Keep the JSON array of {name,description} output shape.",
|
|
28860
|
+
requiredVars: ["current", "specContext"]
|
|
28861
|
+
},
|
|
28862
|
+
{
|
|
28863
|
+
key: "web.spec.subagent_desc",
|
|
28864
|
+
ownerApp: "web",
|
|
28865
|
+
phase: 2,
|
|
28866
|
+
title: "Subagent description suggest",
|
|
28867
|
+
description: "Writes one subagent's description (plain text, no JSON).",
|
|
28868
|
+
requiredVars: ["who", "specContext"]
|
|
28869
|
+
},
|
|
28870
|
+
{
|
|
28871
|
+
key: "web.template.analyze",
|
|
28872
|
+
ownerApp: "web",
|
|
28873
|
+
phase: 2,
|
|
28874
|
+
title: "Template update \u2014 analyze impact",
|
|
28875
|
+
description: "Read-only: reports what a project-template update changes + affected files + risk.",
|
|
28876
|
+
requiredVars: ["localVersion", "latestVersion", "changelog"]
|
|
28877
|
+
},
|
|
28878
|
+
{
|
|
28879
|
+
key: "web.template.update",
|
|
28880
|
+
ownerApp: "web",
|
|
28881
|
+
phase: 2,
|
|
28882
|
+
title: "Template update \u2014 apply (branch + PR)",
|
|
28883
|
+
description: "Agent-write: applies the template update on a branch + opens a PR. Keep the branch/PR steps + the final JSON status block.",
|
|
28884
|
+
requiredVars: ["latestVersion", "branch", "baseRef", "changelog"]
|
|
28885
|
+
},
|
|
28886
|
+
{
|
|
28887
|
+
key: "web.checklist.eval",
|
|
28888
|
+
ownerApp: "web",
|
|
28889
|
+
phase: 2,
|
|
28890
|
+
title: "Checklist evaluate",
|
|
28891
|
+
description: "Read-only: verdict (pass/fail/skip/na) per checklist item vs content. Keep the one-line-per-item reply format.",
|
|
28892
|
+
requiredVars: ["contextLabel", "name", "lines", "contextText"]
|
|
28893
|
+
},
|
|
28894
|
+
{
|
|
28895
|
+
key: "web.checklist.author",
|
|
28896
|
+
ownerApp: "web",
|
|
28897
|
+
phase: 2,
|
|
28898
|
+
title: "Checklist authoring",
|
|
28899
|
+
description: "Proposes the full checklist item list from an instruction + current items. Keep the JSON array output shape.",
|
|
28900
|
+
requiredVars: ["current", "instruction"]
|
|
28901
|
+
},
|
|
28902
|
+
{
|
|
28903
|
+
key: "web.memo.compose",
|
|
28904
|
+
ownerApp: "web",
|
|
28905
|
+
phase: 2,
|
|
28906
|
+
title: "Memo \u2192 community post",
|
|
28907
|
+
description: "Turns a teammate's notes (with [Image#N] tokens) into one forum post. Keep the fenced JSON {title,body} output.",
|
|
28908
|
+
requiredVars: ["notes"]
|
|
28909
|
+
},
|
|
28910
|
+
{
|
|
28911
|
+
key: "web.verify",
|
|
28912
|
+
ownerApp: "web",
|
|
28913
|
+
phase: 2,
|
|
28914
|
+
title: "AI Verify (book entries)",
|
|
28915
|
+
description: "Read-only: answers a question per selected book entry. Keep the one-line-per-entry reply format.",
|
|
28916
|
+
requiredVars: ["verifyBook", "entries", "question"]
|
|
28917
|
+
},
|
|
28918
|
+
{
|
|
28919
|
+
key: "web.git.resolve",
|
|
28920
|
+
ownerApp: "web",
|
|
28921
|
+
phase: 2,
|
|
28922
|
+
title: "Git \u2014 resolve conflicts",
|
|
28923
|
+
description: "Agent: resolves merge conflicts + stages (no commit).",
|
|
28924
|
+
requiredVars: ["repoScope"]
|
|
28925
|
+
}
|
|
28926
|
+
];
|
|
28927
|
+
var AI_PROMPTS = Object.fromEntries(
|
|
28928
|
+
PROMPT_META.map((m2) => [m2.key, { ...m2, defaultTemplate: DEFAULTS[m2.key] ?? "" }])
|
|
28929
|
+
);
|
|
28930
|
+
|
|
28572
28931
|
// vendor/@4pm/constants/error-codes.ts
|
|
28573
28932
|
var ErrorCode = {
|
|
28574
28933
|
// Common
|
|
@@ -28587,6 +28946,11 @@ var ErrorCode = {
|
|
|
28587
28946
|
FEATURE_NOT_AVAILABLE: "FEATURE_NOT_AVAILABLE",
|
|
28588
28947
|
// Request body exceeded the HTTP gateway's JSON size limit (see HTTP_JSON_BODY_LIMIT_BYTES).
|
|
28589
28948
|
PAYLOAD_TOO_LARGE: "PAYLOAD_TOO_LARGE",
|
|
28949
|
+
// Unified composer attachments (ADR-0388): an upload over ATTACHMENT_MAX_BYTES, a MIME outside the
|
|
28950
|
+
// image + file lists, or a submit carrying more than ATTACHMENT_MAX_COUNT refs.
|
|
28951
|
+
ATTACHMENT_TOO_LARGE: "ATTACHMENT_TOO_LARGE",
|
|
28952
|
+
ATTACHMENT_TYPE_UNSUPPORTED: "ATTACHMENT_TYPE_UNSUPPORTED",
|
|
28953
|
+
ATTACHMENT_LIMIT_EXCEEDED: "ATTACHMENT_LIMIT_EXCEEDED",
|
|
28590
28954
|
INTERNAL_ERROR: "INTERNAL_ERROR",
|
|
28591
28955
|
// auth
|
|
28592
28956
|
INVALID_CREDENTIALS: "INVALID_CREDENTIALS",
|
|
@@ -28715,6 +29079,9 @@ var ErrorCode = {
|
|
|
28715
29079
|
// Change-request (ADR-0379) not found / already resolved.
|
|
28716
29080
|
CHANGE_REQUEST_NOT_FOUND: "CHANGE_REQUEST_NOT_FOUND",
|
|
28717
29081
|
CHANGE_REQUEST_ALREADY_RESOLVED: "CHANGE_REQUEST_ALREADY_RESOLVED",
|
|
29082
|
+
// org AI Research (ADR-0380): question refused by the content policy / research-guard; row not found.
|
|
29083
|
+
RESEARCH_REFUSED: "RESEARCH_REFUSED",
|
|
29084
|
+
RESEARCH_NOT_FOUND: "RESEARCH_NOT_FOUND",
|
|
28718
29085
|
MACHINE_ALREADY_IN_POOL: "MACHINE_ALREADY_IN_POOL",
|
|
28719
29086
|
NOT_MACHINE_USER: "NOT_MACHINE_USER",
|
|
28720
29087
|
// worker · physic project
|
|
@@ -28751,6 +29118,14 @@ var ErrorCode = {
|
|
|
28751
29118
|
// web blocks the dispatch and shows an actionable "spec too large" message instead of letting
|
|
28752
29119
|
// the server reject it as a confusing VALIDATION_FAILED.
|
|
28753
29120
|
AI_PROMPT_TOO_LARGE: "AI_PROMPT_TOO_LARGE",
|
|
29121
|
+
// Server-managed prompt catalog (ADR-0381): the key is not in the @4pm/constants registry.
|
|
29122
|
+
AI_PROMPT_KEY_UNKNOWN: "AI_PROMPT_KEY_UNKNOWN",
|
|
29123
|
+
// The submitted template is missing a required `{{var}}` the builder depends on.
|
|
29124
|
+
AI_PROMPT_TEMPLATE_INVALID: "AI_PROMPT_TEMPLATE_INVALID",
|
|
29125
|
+
// Prompt editing is disabled by the AI_PROMPTS_READONLY env flag (use the defaults).
|
|
29126
|
+
AI_PROMPTS_READONLY: "AI_PROMPTS_READONLY",
|
|
29127
|
+
// Terms & policies editing is disabled by the LEGAL_READONLY env flag.
|
|
29128
|
+
LEGAL_READONLY: "LEGAL_READONLY",
|
|
28754
29129
|
REPO_NOT_FOUND: "REPO_NOT_FOUND",
|
|
28755
29130
|
// invitation
|
|
28756
29131
|
INVITATION_TOKEN_INVALID: "INVITATION_TOKEN_INVALID",
|
|
@@ -29932,8 +30307,8 @@ async function runLink(serverUrl, explicitProfile, token = null) {
|
|
|
29932
30307
|
}
|
|
29933
30308
|
|
|
29934
30309
|
// src/commands/start.ts
|
|
29935
|
-
import { spawn as
|
|
29936
|
-
import { join as
|
|
30310
|
+
import { spawn as spawn12 } from "child_process";
|
|
30311
|
+
import { join as join43 } from "path";
|
|
29937
30312
|
|
|
29938
30313
|
// src/core/update.ts
|
|
29939
30314
|
import { execSync as execSync2 } from "child_process";
|
|
@@ -33152,7 +33527,7 @@ async function checkAndUpdate(serverUrl, autoUpdate) {
|
|
|
33152
33527
|
// src/core/ws-client.ts
|
|
33153
33528
|
init_wrapper();
|
|
33154
33529
|
import { randomUUID as randomUUID4 } from "crypto";
|
|
33155
|
-
import { basename as basename7, join as
|
|
33530
|
+
import { basename as basename7, join as join38 } from "path";
|
|
33156
33531
|
import { existsSync as existsSync19, mkdirSync as mkdirSync9 } from "fs";
|
|
33157
33532
|
|
|
33158
33533
|
// vendor/@4pm/utils/helpers.ts
|
|
@@ -33185,6 +33560,59 @@ function prettyResult(text) {
|
|
|
33185
33560
|
return trimmed;
|
|
33186
33561
|
}
|
|
33187
33562
|
|
|
33563
|
+
// vendor/@4pm/dto/attachment.ts
|
|
33564
|
+
var ATTACHMENT_MAX_BYTES = 10 * 1024 * 1024;
|
|
33565
|
+
var ATTACHMENT_MAX_COUNT = 10;
|
|
33566
|
+
var ATTACHMENT_IMAGE_MIME_TYPES = ["image/png", "image/jpeg", "image/webp", "image/gif"];
|
|
33567
|
+
var ATTACHMENT_FILE_MIME_TYPES = [
|
|
33568
|
+
"application/pdf",
|
|
33569
|
+
"application/zip",
|
|
33570
|
+
"application/x-zip-compressed",
|
|
33571
|
+
"application/msword",
|
|
33572
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
33573
|
+
"application/vnd.ms-excel",
|
|
33574
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
33575
|
+
"text/plain",
|
|
33576
|
+
"text/csv",
|
|
33577
|
+
"application/json"
|
|
33578
|
+
];
|
|
33579
|
+
var ATTACHMENT_FILE_EXTENSIONS = [".pdf", ".zip", ".doc", ".docx", ".xls", ".xlsx", ".txt", ".csv", ".json"];
|
|
33580
|
+
var ATTACHMENT_ACCEPT = [...ATTACHMENT_IMAGE_MIME_TYPES, ...ATTACHMENT_FILE_MIME_TYPES, ...ATTACHMENT_FILE_EXTENSIONS].join(",");
|
|
33581
|
+
function attachmentExt(mime) {
|
|
33582
|
+
switch (mime) {
|
|
33583
|
+
case "image/png":
|
|
33584
|
+
return "png";
|
|
33585
|
+
case "image/jpeg":
|
|
33586
|
+
return "jpg";
|
|
33587
|
+
case "image/webp":
|
|
33588
|
+
return "webp";
|
|
33589
|
+
case "image/gif":
|
|
33590
|
+
return "gif";
|
|
33591
|
+
case "application/pdf":
|
|
33592
|
+
return "pdf";
|
|
33593
|
+
case "application/zip":
|
|
33594
|
+
case "application/x-zip-compressed":
|
|
33595
|
+
return "zip";
|
|
33596
|
+
case "application/msword":
|
|
33597
|
+
return "doc";
|
|
33598
|
+
case "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
|
|
33599
|
+
return "docx";
|
|
33600
|
+
case "application/vnd.ms-excel":
|
|
33601
|
+
return "xls";
|
|
33602
|
+
case "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":
|
|
33603
|
+
return "xlsx";
|
|
33604
|
+
case "text/plain":
|
|
33605
|
+
return "txt";
|
|
33606
|
+
case "text/csv":
|
|
33607
|
+
return "csv";
|
|
33608
|
+
case "application/json":
|
|
33609
|
+
return "json";
|
|
33610
|
+
default:
|
|
33611
|
+
return null;
|
|
33612
|
+
}
|
|
33613
|
+
}
|
|
33614
|
+
var ATTACHMENT_ID_EXT_PATTERN = "png|jpg|webp|gif|pdf|zip|doc|docx|xls|xlsx|txt|csv|json";
|
|
33615
|
+
|
|
33188
33616
|
// ../../node_modules/.pnpm/zod@3.25.76/node_modules/zod/v3/external.js
|
|
33189
33617
|
var external_exports = {};
|
|
33190
33618
|
__export(external_exports, {
|
|
@@ -37240,23 +37668,13 @@ var deletedFilterSchema = external_exports.enum(["true", "false"]).transform((v2
|
|
|
37240
37668
|
// vendor/@4pm/dto/command.ts
|
|
37241
37669
|
var COMMAND_MAX_LEN = 8e3;
|
|
37242
37670
|
var AI_PROMPT_MAX_LEN = 5e5;
|
|
37243
|
-
var
|
|
37244
|
-
var
|
|
37245
|
-
var
|
|
37246
|
-
|
|
37671
|
+
var COMMAND_IMAGE_MAX_COUNT = ATTACHMENT_MAX_COUNT;
|
|
37672
|
+
var COMMAND_IMAGE_MIME_TYPES = [...ATTACHMENT_IMAGE_MIME_TYPES, ...ATTACHMENT_FILE_MIME_TYPES];
|
|
37673
|
+
var COMMAND_IMAGE_ID_RE = new RegExp(
|
|
37674
|
+
`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\\.(${ATTACHMENT_ID_EXT_PATTERN})$`
|
|
37675
|
+
);
|
|
37247
37676
|
function commandImageExt(mime) {
|
|
37248
|
-
|
|
37249
|
-
case "image/png":
|
|
37250
|
-
return "png";
|
|
37251
|
-
case "image/jpeg":
|
|
37252
|
-
return "jpg";
|
|
37253
|
-
case "image/webp":
|
|
37254
|
-
return "webp";
|
|
37255
|
-
case "image/gif":
|
|
37256
|
-
return "gif";
|
|
37257
|
-
default:
|
|
37258
|
-
return "bin";
|
|
37259
|
-
}
|
|
37677
|
+
return attachmentExt(mime) ?? "bin";
|
|
37260
37678
|
}
|
|
37261
37679
|
var commandImageRefSchema = external_exports.object({
|
|
37262
37680
|
// Constrained to `<uuidv4>.<ext>` (the only id the upload mints) so a client-supplied id can never
|
|
@@ -37264,7 +37682,9 @@ var commandImageRefSchema = external_exports.object({
|
|
|
37264
37682
|
id: external_exports.string().regex(COMMAND_IMAGE_ID_RE),
|
|
37265
37683
|
placeholder: external_exports.string().min(1).max(40),
|
|
37266
37684
|
name: external_exports.string().max(255),
|
|
37267
|
-
mime: external_exports.enum(COMMAND_IMAGE_MIME_TYPES)
|
|
37685
|
+
mime: external_exports.enum(COMMAND_IMAGE_MIME_TYPES),
|
|
37686
|
+
/** Image or file (ADR-0388); omitted by older clients ⇒ derived from `mime`. */
|
|
37687
|
+
category: external_exports.enum(["image", "file"]).optional()
|
|
37268
37688
|
});
|
|
37269
37689
|
var aiRunConfigSchema = external_exports.object({
|
|
37270
37690
|
/** `--model` override for this run (must match the resolved provider's model id). */
|
|
@@ -37441,6 +37861,14 @@ var putGitCredentialRequestSchema = external_exports.object({
|
|
|
37441
37861
|
ctx.addIssue({ code: external_exports.ZodIssueCode.custom, path: ["host"], message: "host is required for GitHub Enterprise" });
|
|
37442
37862
|
}
|
|
37443
37863
|
});
|
|
37864
|
+
var putGitLabCredentialRequestSchema = external_exports.object({
|
|
37865
|
+
host: hostSchema.optional(),
|
|
37866
|
+
group: external_exports.string().trim().min(1).max(200),
|
|
37867
|
+
/** The access token; omit to keep the stored one (required on first save — enforced server-side). */
|
|
37868
|
+
token: external_exports.string().trim().min(1).max(512).optional(),
|
|
37869
|
+
apiBaseUrl: external_exports.string().trim().url().startsWith("https://").max(500).nullable().optional(),
|
|
37870
|
+
caCert: external_exports.string().trim().max(32768).nullable().optional()
|
|
37871
|
+
});
|
|
37444
37872
|
function parseRepoUrl(url) {
|
|
37445
37873
|
const raw = url.trim();
|
|
37446
37874
|
const scp = /^[\w.-]+@([^:/\s]+):([^/\s]+)\/(.+?)(?:\.git)?\/?$/.exec(raw);
|
|
@@ -38074,6 +38502,23 @@ import { tmpdir as tmpdir2 } from "os";
|
|
|
38074
38502
|
import { dirname as dirname3, join as join6, resolve } from "path";
|
|
38075
38503
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
38076
38504
|
import { promisify } from "util";
|
|
38505
|
+
|
|
38506
|
+
// src/core/prompt-overrides.ts
|
|
38507
|
+
var overrides = {};
|
|
38508
|
+
function setPromptOverrides(map) {
|
|
38509
|
+
overrides = map ?? {};
|
|
38510
|
+
}
|
|
38511
|
+
function getPromptOverride(key) {
|
|
38512
|
+
const byLocale = overrides[key];
|
|
38513
|
+
if (!byLocale) return null;
|
|
38514
|
+
return byLocale[getLocale()] ?? byLocale.en ?? null;
|
|
38515
|
+
}
|
|
38516
|
+
function resolveCliPrompt(key, vars) {
|
|
38517
|
+
const template = getPromptOverride(key) ?? AI_PROMPTS[key]?.defaultTemplate ?? "";
|
|
38518
|
+
return renderPrompt(template, vars);
|
|
38519
|
+
}
|
|
38520
|
+
|
|
38521
|
+
// src/core/scaffold.ts
|
|
38077
38522
|
var run = promisify(execFile);
|
|
38078
38523
|
function reposOf(spec) {
|
|
38079
38524
|
const repos = spec?.repos;
|
|
@@ -38489,18 +38934,22 @@ ${(sa.description || "").trim()}
|
|
|
38489
38934
|
generate3,
|
|
38490
38935
|
join6(target, "README.md"),
|
|
38491
38936
|
"README.md",
|
|
38492
|
-
|
|
38493
|
-
|
|
38937
|
+
// Admin override (ADR-0381) for `cli.scaffold.readme`, else the shared registry default.
|
|
38938
|
+
resolveCliPrompt("cli.scaffold.readme", { specJson })
|
|
38494
38939
|
);
|
|
38495
38940
|
await generateFile(
|
|
38496
38941
|
generate3,
|
|
38497
38942
|
join6(target, guideFile),
|
|
38498
38943
|
guideFile,
|
|
38499
|
-
|
|
38500
|
-
|
|
38944
|
+
// Admin override (ADR-0381) for `cli.scaffold.guide`, else the shared registry default.
|
|
38945
|
+
resolveCliPrompt("cli.scaffold.guide", {
|
|
38946
|
+
guideFile,
|
|
38947
|
+
specJson,
|
|
38948
|
+
guideInstructionsBlock: guideInstructions ? `
|
|
38501
38949
|
Also incorporate these additional instructions/content:
|
|
38502
38950
|
${guideInstructions}
|
|
38503
|
-
` : ""
|
|
38951
|
+
` : ""
|
|
38952
|
+
}),
|
|
38504
38953
|
guideInstructions
|
|
38505
38954
|
);
|
|
38506
38955
|
} finally {
|
|
@@ -39022,6 +39471,15 @@ var WsChannels = {
|
|
|
39022
39471
|
// tools, empty dir — like support-answer) and replies with the raw output for the web to parse. No
|
|
39023
39472
|
// project is involved (checklists are org-scoped).
|
|
39024
39473
|
CHECKLIST_AUTHOR: "checklist.author",
|
|
39474
|
+
// Org-level "AI Research" (ADR-0380): server (picks an idle cli of the org AI pool) → cli
|
|
39475
|
+
// request–reply. The server forwards the user's question; the worker runs a one-shot research agent
|
|
39476
|
+
// (read-only web/search tools) under a content policy + a research-guard post-check, and replies with
|
|
39477
|
+
// the markdown answer (or a structured refusal) plus the run's token usage so the server meters it to
|
|
39478
|
+
// the org (projectId=null). No project is involved.
|
|
39479
|
+
RESEARCH_ASK: "research.ask",
|
|
39480
|
+
// Org AI Research live progress (ADR-0380): cli → server one-way stream of answer chunks / status for
|
|
39481
|
+
// a running research question, keyed by the query id, relayed to the web over SSE.
|
|
39482
|
+
RESEARCH_PROGRESS: "research.progress",
|
|
39025
39483
|
// Shared AI memory (ADR-0245): cli → server one-way write-back of the compacted rolling memory for
|
|
39026
39484
|
// the project it serves (persisted per project × machine-user link). The read direction rides
|
|
39027
39485
|
// `ws_token` (seeds the cli cache on connect); this channel only carries updates.
|
|
@@ -40268,7 +40726,7 @@ async function materializeImages(physicRoot, commandId, images, fetch2) {
|
|
|
40268
40726
|
continue;
|
|
40269
40727
|
}
|
|
40270
40728
|
const ext = commandImageExt(reply.mime ?? img.mime);
|
|
40271
|
-
const file = join12(dir2, `
|
|
40729
|
+
const file = join12(dir2, `attachment-${n}.${ext}`);
|
|
40272
40730
|
writeFileSync6(file, Buffer.from(reply.dataBase64, "base64"));
|
|
40273
40731
|
map.set(img.placeholder, file);
|
|
40274
40732
|
} catch (err) {
|
|
@@ -40526,13 +40984,13 @@ var ENV_ALLOW = /* @__PURE__ */ new Set([
|
|
|
40526
40984
|
"PROGRAMFILES"
|
|
40527
40985
|
]);
|
|
40528
40986
|
var ENV_ALLOW_PREFIXES = ["LC_", "XDG_", "ANTHROPIC_", "CLAUDE_", "CODEX_", "OPENAI_", "AWS_", "GOOGLE_", "CLOUD_ML_", "VERTEX_"];
|
|
40529
|
-
function agentEnv(extraEnv,
|
|
40987
|
+
function agentEnv(extraEnv, overrides2) {
|
|
40530
40988
|
const env3 = {};
|
|
40531
40989
|
for (const [key, value] of Object.entries(process.env)) {
|
|
40532
40990
|
if (value === void 0) continue;
|
|
40533
40991
|
if (ENV_ALLOW.has(key) || ENV_ALLOW_PREFIXES.some((p2) => key.startsWith(p2))) env3[key] = value;
|
|
40534
40992
|
}
|
|
40535
|
-
return { ...env3, ...extraEnv, ...
|
|
40993
|
+
return { ...env3, ...extraEnv, ...overrides2 };
|
|
40536
40994
|
}
|
|
40537
40995
|
function gitAuthArgs(user, password) {
|
|
40538
40996
|
if (!user && !password) return [];
|
|
@@ -40628,106 +41086,46 @@ ${body}`;
|
|
|
40628
41086
|
}
|
|
40629
41087
|
function buildPrompt(docs, question, askerRole) {
|
|
40630
41088
|
const roleNote = askerRole === "admin" ? "The asker is a platform admin." : "The asker is a regular user \u2014 do NOT reveal admin-only features.";
|
|
40631
|
-
return
|
|
40632
|
-
"You are the 4PM product support assistant. Answer the user's question about how to use 4PM",
|
|
40633
|
-
"using ONLY the documentation and FAQ provided below. If the answer is not in the docs, say",
|
|
40634
|
-
"you don't have that information and suggest contacting human support \u2014 do not invent details.",
|
|
40635
|
-
roleNote,
|
|
40636
|
-
"Answer concisely in the user's language; reference the relevant doc heading/path when useful.",
|
|
40637
|
-
"",
|
|
40638
|
-
// Structured output for inline moderation (ADR-0237): the same run also classifies the question.
|
|
40639
|
-
"Return ONLY a single JSON object (no markdown fences, no prose around it) with these keys:",
|
|
40640
|
-
' "onTopic": boolean \u2014 false if the question is NOT about how to use the 4PM product,',
|
|
40641
|
-
' "sensitive": boolean \u2014 true if the question is inappropriate / abusive OR asks how to hack,',
|
|
40642
|
-
" bypass security, gain unauthorized access, take over or steal another",
|
|
40643
|
-
" user's/org's account or data, obtain credentials, or any malicious intent,",
|
|
40644
|
-
' "reason": string \u2014 a short reason when onTopic is false or sensitive is true, else "",',
|
|
40645
|
-
` "answer": string \u2014 your reply to the user, in markdown (in the user's language).`,
|
|
40646
|
-
"Refusal policy (ADR-0237): if the question is off-topic (onTopic false) OR sensitive, DO NOT",
|
|
40647
|
-
`answer it. Set "answer" to this EXACT same polite refusal for BOTH cases (in the user's`,
|
|
40648
|
-
'language, do not reveal which category or hint at the reason): "Sorry, I can only help with',
|
|
40649
|
-
'questions about how to use 4PM. For anything else, please contact human support." Only answer',
|
|
40650
|
-
"normally when onTopic is true AND sensitive is false.",
|
|
40651
|
-
"",
|
|
40652
|
-
"===== DOCUMENTATION =====",
|
|
40653
|
-
docs,
|
|
40654
|
-
"",
|
|
40655
|
-
"===== QUESTION =====",
|
|
40656
|
-
question
|
|
40657
|
-
].join("\n");
|
|
41089
|
+
return resolveCliPrompt("cli.support.answer", { roleNote, docs, question });
|
|
40658
41090
|
}
|
|
40659
41091
|
function buildDraftPrompt(docs, context, task) {
|
|
40660
|
-
if (task === "
|
|
40661
|
-
|
|
40662
|
-
|
|
40663
|
-
|
|
40664
|
-
|
|
40665
|
-
|
|
40666
|
-
|
|
40667
|
-
"Use the subject and any current draft below as the starting point. Be clear, friendly and concise.",
|
|
40668
|
-
"Write in the language of the subject/draft (or of the admin's instructions when those are empty)."
|
|
40669
|
-
];
|
|
41092
|
+
if (task === "prompt_generate") return buildPromptGeneratePrompt(context);
|
|
41093
|
+
if (task === "prompt_translate") return buildPromptTranslatePrompt(context);
|
|
41094
|
+
if (task === "legal_draft") return resolveCliPrompt("cli.support.legal_draft", { docs, context });
|
|
41095
|
+
const key = task === "reply_draft" ? "cli.support.reply_draft" : "cli.support.outreach_draft";
|
|
41096
|
+
return resolveCliPrompt(key, { docs, context });
|
|
41097
|
+
}
|
|
41098
|
+
function buildPromptGeneratePrompt(context) {
|
|
40670
41099
|
return [
|
|
40671
|
-
|
|
40672
|
-
"
|
|
40673
|
-
"
|
|
40674
|
-
"
|
|
40675
|
-
"
|
|
40676
|
-
"",
|
|
40677
|
-
"Output ONLY the message body in markdown \u2014 no subject line, no preamble, no explanation, no code",
|
|
40678
|
-
"fences around the whole message.",
|
|
40679
|
-
"",
|
|
40680
|
-
"===== DOCUMENTATION =====",
|
|
40681
|
-
docs,
|
|
41100
|
+
"You are writing a reusable AI PROMPT TEMPLATE for the 4PM platform. Based on the specification",
|
|
41101
|
+
"below, write a clear, effective prompt. Include each required {{placeholder}} verbatim where",
|
|
41102
|
+
"appropriate \u2014 never rename or invent placeholders. A human admin reviews your draft before it",
|
|
41103
|
+
"goes live.",
|
|
41104
|
+
"Output ONLY the prompt template text \u2014 no preamble, no explanation, no code fences.",
|
|
40682
41105
|
"",
|
|
40683
|
-
"=====
|
|
41106
|
+
"===== SPECIFICATION =====",
|
|
40684
41107
|
context
|
|
40685
41108
|
].join("\n");
|
|
40686
41109
|
}
|
|
40687
|
-
function
|
|
41110
|
+
function buildPromptTranslatePrompt(context) {
|
|
40688
41111
|
return [
|
|
40689
|
-
"You are
|
|
40690
|
-
"
|
|
40691
|
-
"
|
|
40692
|
-
"
|
|
40693
|
-
"When the target locale is not English and an English reference is given, follow its meaning closely.",
|
|
40694
|
-
"Rules:",
|
|
40695
|
-
"- Every {{placeholder}} (e.g. {{companyName}}) must be kept verbatim \u2014 never fill or rename it.",
|
|
40696
|
-
"- Describe plans, prices, upgrades, downgrades, cancellations, renewals, refunds and add-ons ONLY",
|
|
40697
|
-
" as stated in the PLAN CATALOG and BILLING FACTS below or the documentation; never invent",
|
|
40698
|
-
" commitments, prices or legal guarantees. Where something is unknown write `[TODO: \u2026]`.",
|
|
40699
|
-
"- Plain, precise legal English (or the target language); short numbered sections and bullet lists.",
|
|
40700
|
-
"- This is a draft, not legal advice; do not add a disclaimer about that inside the document.",
|
|
41112
|
+
"You are translating a reusable AI PROMPT TEMPLATE for the 4PM platform into the target language",
|
|
41113
|
+
"given below. Preserve EVERY {{placeholder}} exactly \u2014 never translate, rename, or remove a",
|
|
41114
|
+
"placeholder. Keep the same structure, intent and formatting. A human admin reviews your draft.",
|
|
41115
|
+
"Output ONLY the translated template text \u2014 no preamble, no explanation, no code fences.",
|
|
40701
41116
|
"",
|
|
40702
|
-
"
|
|
40703
|
-
"fences around the whole document.",
|
|
40704
|
-
"",
|
|
40705
|
-
"===== DOCUMENTATION =====",
|
|
40706
|
-
docs,
|
|
40707
|
-
"",
|
|
40708
|
-
"===== CONTEXT =====",
|
|
41117
|
+
"===== SPECIFICATION =====",
|
|
40709
41118
|
context
|
|
40710
41119
|
].join("\n");
|
|
40711
41120
|
}
|
|
40712
41121
|
function imageExt(mime) {
|
|
40713
|
-
|
|
40714
|
-
case "image/png":
|
|
40715
|
-
return "png";
|
|
40716
|
-
case "image/jpeg":
|
|
40717
|
-
return "jpg";
|
|
40718
|
-
case "image/webp":
|
|
40719
|
-
return "webp";
|
|
40720
|
-
case "image/gif":
|
|
40721
|
-
return "gif";
|
|
40722
|
-
default:
|
|
40723
|
-
return "png";
|
|
40724
|
-
}
|
|
41122
|
+
return commandImageExt(mime);
|
|
40725
41123
|
}
|
|
40726
41124
|
function materializeImages2(images, question) {
|
|
40727
41125
|
const dir2 = mkdtempSync2(join14(tmpdir3(), "4pm-help-images-"));
|
|
40728
41126
|
let rewritten = question;
|
|
40729
41127
|
images.forEach((img, i) => {
|
|
40730
|
-
const file = join14(dir2, `
|
|
41128
|
+
const file = join14(dir2, `attachment-${i + 1}.${imageExt(img.mime)}`);
|
|
40731
41129
|
writeFileSync7(file, Buffer.from(img.dataBase64, "base64"));
|
|
40732
41130
|
if (img.placeholder) rewritten = rewritten.split(img.placeholder).join(file);
|
|
40733
41131
|
});
|
|
@@ -40823,12 +41221,12 @@ async function runClaudeWithFailover(prompt, ai2, imageDir, timeoutMs = ANSWER_T
|
|
|
40823
41221
|
}
|
|
40824
41222
|
async function runChecklistAuthor(req, ai2) {
|
|
40825
41223
|
try {
|
|
40826
|
-
const { text } = await runClaudeWithFailover(req.prompt, ai2);
|
|
40827
|
-
if (!text) return { error: "empty output" };
|
|
40828
|
-
return { output: text };
|
|
41224
|
+
const { text, usage } = await runClaudeWithFailover(req.prompt, ai2);
|
|
41225
|
+
if (!text) return { reply: { error: "empty output" }, usage };
|
|
41226
|
+
return { reply: { output: text }, usage };
|
|
40829
41227
|
} catch (err) {
|
|
40830
41228
|
logger.warn("checklist.author.failed", { error: String(err) });
|
|
40831
|
-
return { error: String(err) };
|
|
41229
|
+
return { reply: { error: String(err) }, usage: { ...NO_USAGE } };
|
|
40832
41230
|
}
|
|
40833
41231
|
}
|
|
40834
41232
|
async function runSupportAnswer(req, ai2, profileDir2) {
|
|
@@ -41880,6 +42278,24 @@ fi
|
|
|
41880
42278
|
exec "${realGh}" "$@"
|
|
41881
42279
|
`;
|
|
41882
42280
|
}
|
|
42281
|
+
function findRealGlab(pathValue, shimDir) {
|
|
42282
|
+
for (const dir2 of pathValue.split(delimiter)) {
|
|
42283
|
+
if (!dir2 || dir2 === shimDir) continue;
|
|
42284
|
+
const candidate = join20(dir2, process.platform === "win32" ? "glab.exe" : "glab");
|
|
42285
|
+
if (existsSync13(candidate)) return candidate;
|
|
42286
|
+
}
|
|
42287
|
+
return null;
|
|
42288
|
+
}
|
|
42289
|
+
function glabShimScript(realGlab, cli) {
|
|
42290
|
+
return `#!/bin/sh
|
|
42291
|
+
# 4PM glab shim (ADR-0382) \u2014 injects the served project's GitLab group token.
|
|
42292
|
+
if [ -z "$GITLAB_TOKEN" ]; then
|
|
42293
|
+
T="$(${cli} git-token 2>/dev/null)"
|
|
42294
|
+
if [ -n "$T" ]; then export GITLAB_HOST="$${GIT_HOST_ENV}" GITLAB_TOKEN="$T"; fi
|
|
42295
|
+
fi
|
|
42296
|
+
exec "${realGlab}" "$@"
|
|
42297
|
+
`;
|
|
42298
|
+
}
|
|
41883
42299
|
function clearEnv() {
|
|
41884
42300
|
if (!state.baseEnv) return;
|
|
41885
42301
|
const { count, path } = state.baseEnv;
|
|
@@ -41896,7 +42312,8 @@ function clearEnv() {
|
|
|
41896
42312
|
}
|
|
41897
42313
|
function configureGitAuth(method, host, profileDir2, transport) {
|
|
41898
42314
|
try {
|
|
41899
|
-
const
|
|
42315
|
+
const isGitlab = method === "gitlab-group-token";
|
|
42316
|
+
const active = (method === "github-app" || isGitlab) && !!host;
|
|
41900
42317
|
if (!active || state.host && state.host !== host) {
|
|
41901
42318
|
if (state.host) void revokeAll();
|
|
41902
42319
|
clearEnv();
|
|
@@ -41925,22 +42342,26 @@ function configureGitAuth(method, host, profileDir2, transport) {
|
|
|
41925
42342
|
process.env.TSX_TSCONFIG_PATH = devTsconfig;
|
|
41926
42343
|
}
|
|
41927
42344
|
process.env[GIT_HOST_ENV] = host;
|
|
41928
|
-
process.env[GIT_HOST_KIND_ENV] = githubHostKind(host);
|
|
42345
|
+
process.env[GIT_HOST_KIND_ENV] = isGitlab ? "gitlab" : githubHostKind(host);
|
|
41929
42346
|
if (process.platform !== "win32") {
|
|
41930
42347
|
const shimDir = join20(profileDir2, "bin");
|
|
41931
|
-
const
|
|
41932
|
-
|
|
42348
|
+
const toolName = isGitlab ? "glab" : "gh";
|
|
42349
|
+
const otherTool = isGitlab ? "gh" : "glab";
|
|
42350
|
+
const realTool = isGitlab ? findRealGlab(state.baseEnv.path, shimDir) : findRealGh(state.baseEnv.path, shimDir);
|
|
42351
|
+
if (realTool) {
|
|
41933
42352
|
mkdirSync6(shimDir, { recursive: true });
|
|
41934
|
-
const shim = join20(shimDir,
|
|
41935
|
-
|
|
42353
|
+
const shim = join20(shimDir, toolName);
|
|
42354
|
+
const shimContent = isGitlab ? glabShimScript(realTool, cliInvocation()) : shimScript(realTool, cliInvocation());
|
|
42355
|
+
writeFileSync8(shim, shimContent, { encoding: "utf8", mode: 493 });
|
|
41936
42356
|
chmodSync3(shim, 493);
|
|
41937
42357
|
process.env.PATH = `${shimDir}${delimiter}${state.baseEnv.path}`;
|
|
41938
42358
|
} else {
|
|
41939
|
-
rmSync6(join20(shimDir,
|
|
42359
|
+
rmSync6(join20(shimDir, toolName), { force: true });
|
|
41940
42360
|
}
|
|
42361
|
+
rmSync6(join20(shimDir, otherTool), { force: true });
|
|
41941
42362
|
}
|
|
41942
42363
|
state.host = host;
|
|
41943
|
-
logger.info("git.auth.github-app", { host });
|
|
42364
|
+
logger.info(isGitlab ? "git.auth.gitlab" : "git.auth.github-app", { host });
|
|
41944
42365
|
return true;
|
|
41945
42366
|
} catch (err) {
|
|
41946
42367
|
logger.warn("git.auth.configure.failed", { error: String(err) });
|
|
@@ -42826,22 +43247,12 @@ function parseJsonResult(out) {
|
|
|
42826
43247
|
}
|
|
42827
43248
|
var COMPACT_TIMEOUT_MS = 6e4;
|
|
42828
43249
|
function buildPrompt2(input) {
|
|
42829
|
-
return
|
|
42830
|
-
|
|
42831
|
-
|
|
42832
|
-
|
|
42833
|
-
|
|
42834
|
-
|
|
42835
|
-
"",
|
|
42836
|
-
"=== EXISTING MEMORY ===",
|
|
42837
|
-
input.oldMemory || "(empty)",
|
|
42838
|
-
"",
|
|
42839
|
-
"=== LATEST USER PROMPT ===",
|
|
42840
|
-
input.prompt,
|
|
42841
|
-
"",
|
|
42842
|
-
"=== LATEST ASSISTANT ANSWER ===",
|
|
42843
|
-
input.answer
|
|
42844
|
-
].join("\n");
|
|
43250
|
+
return resolveCliPrompt("cli.memory.compact", {
|
|
43251
|
+
budgetChars: input.budgetChars,
|
|
43252
|
+
oldMemory: input.oldMemory || "(empty)",
|
|
43253
|
+
prompt: input.prompt,
|
|
43254
|
+
answer: input.answer
|
|
43255
|
+
});
|
|
42845
43256
|
}
|
|
42846
43257
|
function runOnce(cmd, profile, prompt, cwd2, extraEnv, denyDirs = []) {
|
|
42847
43258
|
return new Promise((resolve13, reject) => {
|
|
@@ -43019,10 +43430,10 @@ function activeAiRunCount() {
|
|
|
43019
43430
|
return activeRuns.size;
|
|
43020
43431
|
}
|
|
43021
43432
|
function cancelActiveRun(commandId) {
|
|
43022
|
-
const
|
|
43023
|
-
if (!
|
|
43024
|
-
|
|
43025
|
-
return { ok: true, state:
|
|
43433
|
+
const run16 = activeRuns.get(commandId);
|
|
43434
|
+
if (!run16) return { ok: false, state: "unknown" };
|
|
43435
|
+
run16.controller.abort();
|
|
43436
|
+
return { ok: true, state: run16.state };
|
|
43026
43437
|
}
|
|
43027
43438
|
function handleCommandChannels(ctx, message, payload) {
|
|
43028
43439
|
if (message.channel === WsChannels.COMMAND_CANCEL) {
|
|
@@ -43120,16 +43531,16 @@ async function runLocalCommand(ctx, input) {
|
|
|
43120
43531
|
}
|
|
43121
43532
|
var REFUSED = { exitCode: 1, cancelled: false, exhausted: false, neverStarted: true, output: "" };
|
|
43122
43533
|
async function runAiPrompt(ctx, prompt, commandId, origin, oneShot = false, images, aiConfig, readOnly = false, bypass = false) {
|
|
43123
|
-
const
|
|
43124
|
-
activeRuns.set(commandId,
|
|
43534
|
+
const run16 = { controller: new AbortController(), state: "queued" };
|
|
43535
|
+
activeRuns.set(commandId, run16);
|
|
43125
43536
|
try {
|
|
43126
|
-
return await runAiPromptInner(ctx, prompt, commandId, origin,
|
|
43537
|
+
return await runAiPromptInner(ctx, prompt, commandId, origin, run16, oneShot, images, aiConfig, readOnly, bypass);
|
|
43127
43538
|
} finally {
|
|
43128
43539
|
activeRuns.delete(commandId);
|
|
43129
43540
|
if (!readOnly) requestGitSnapshot("ai-run");
|
|
43130
43541
|
}
|
|
43131
43542
|
}
|
|
43132
|
-
async function runAiPromptInner(ctx, prompt, commandId, origin,
|
|
43543
|
+
async function runAiPromptInner(ctx, prompt, commandId, origin, run16, oneShot = false, images, aiConfig, readOnly = false, bypass = false) {
|
|
43133
43544
|
const config = readProfileConfig(ctx.profileDir);
|
|
43134
43545
|
const projectTimeoutSec = config.projectAiRunTimeoutSec ?? 0;
|
|
43135
43546
|
const machineTimeoutSec = config.aiRunTimeoutSec ?? 0;
|
|
@@ -43312,7 +43723,7 @@ ${guardedPrompt}`;
|
|
|
43312
43723
|
queue: { position, limit, running: running2 }
|
|
43313
43724
|
});
|
|
43314
43725
|
},
|
|
43315
|
-
|
|
43726
|
+
run16.controller.signal
|
|
43316
43727
|
);
|
|
43317
43728
|
if (slot.kind === "timeout") {
|
|
43318
43729
|
neverStarted = true;
|
|
@@ -43333,9 +43744,9 @@ ${guardedPrompt}`;
|
|
|
43333
43744
|
result = { exitCode: COMMAND_CANCELLED_EXIT_CODE, workedDir: null, workedKey: null, workedCmd: null, usage: { tokens: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 }, sessionId: "", cancelled: true };
|
|
43334
43745
|
} else {
|
|
43335
43746
|
if (slot.queued) ctx.send(WsChannels.COMMAND_OUTPUT, { commandId, seq: seq++, chunk: "", log: true, queue: null });
|
|
43336
|
-
|
|
43747
|
+
run16.state = "running";
|
|
43337
43748
|
try {
|
|
43338
|
-
result = await runAiFailover(plan, commandId, cwd2, handlers, aiRunTimeoutMs,
|
|
43749
|
+
result = await runAiFailover(plan, commandId, cwd2, handlers, aiRunTimeoutMs, run16.controller.signal);
|
|
43339
43750
|
} finally {
|
|
43340
43751
|
slot.handle.release();
|
|
43341
43752
|
}
|
|
@@ -44100,79 +44511,37 @@ empty template. The single source of truth for approval is \`.claude/.autonomous
|
|
|
44100
44511
|
NEVER act on an unapproved row.`;
|
|
44101
44512
|
function buildIntakePrompt(cfg) {
|
|
44102
44513
|
const h = cfg.taskSizeHints;
|
|
44103
|
-
return
|
|
44104
|
-
|
|
44105
|
-
|
|
44106
|
-
|
|
44107
|
-
|
|
44108
|
-
|
|
44109
|
-
|
|
44110
|
-
Read \`USER_TODO.md\` (columns \`ID | Group | Depends | Request\`). For each APPROVED \`REQ-\u2026\` whose \`Depends\`
|
|
44111
|
-
are already handled:
|
|
44112
|
-
- CLEAR request \u21D2 split it into tasks appended to \`AI_TODO.md\` (columns
|
|
44113
|
-
\`ID | Priority | Tag | Depends | Group | Task description | Notes\`), ids \`TSK-{groupid:0000}-{taskid:0000}\`
|
|
44114
|
-
(groupid UNIQUE + INCREASING across all history \u2014 the largest ever used in \`AI_TODO.md\`/\`AI_DONE.md\`/
|
|
44115
|
-
\`AI_PROGRESS.md\`/their git history, +1; taskid from 0001). \`Depends\` = the \`TSK-\u2026\` it needs.
|
|
44116
|
-
Remove the analysed request row (empty \u21D2 reset to the empty template).
|
|
44117
|
-
- UNCLEAR request \u21D2 do NOT guess: append a \`USER_QA.md\` row (\`ID | Group | Depends | Original request |
|
|
44118
|
-
Question / options | Answer\`) with a new \`QA-{groupid:0000}-{qaid:0000}\`, blank \`Answer\`; remove the request row.
|
|
44119
|
-
An unapproved request stays.
|
|
44120
|
-
|
|
44121
|
-
## 2 \u2014 Answered intake questions
|
|
44122
|
-
For each \`USER_QA.md\` row with an \`Answer\`, APPROVED, and whose id is NOT in any \`AI_TODO.md\` \`Depends\`
|
|
44123
|
-
(those are task questions \u2014 the cli handles them; leave them alone): re-analyse the original request with the
|
|
44124
|
-
answer into tasks (same rules), then remove that QA row.
|
|
44125
|
-
|
|
44126
|
-
## Task size (MANDATORY \u2014 every task must be finishable in ONE run)
|
|
44127
|
-
Size each task by SCOPE and write \`size: S\` or \`size: M\` at the start of \`Notes\`:
|
|
44128
|
-
- **S** \u2014 one small change in one module/layer (\u2248 \u2264 ${h.sMaxFiles} files, \u2248 \u2264 ${h.sMaxLines} lines), e.g. fix a bug, add a field.
|
|
44129
|
-
- **M** \u2014 one feature slice across 2\u20133 layers with ONE purpose (\u2248 \u2264 ${h.mMaxFiles} files, \u2248 \u2264 ${h.mMaxLines} lines), e.g. one CRUD screen, one API + its UI + tests.
|
|
44130
|
-
- **L** \u2014 several features, a cross-cutting change, a big migration, a new subsystem, or an architecture decision.
|
|
44131
|
-
**Never write an L task**: split it into S/M tasks chained with \`Depends\`, or ask in \`USER_QA.md\` when the split needs a decision.
|
|
44132
|
-
The counts are hints, not hard limits. Each task's \`Task description\` states the acceptance criteria and how to test it.
|
|
44133
|
-
|
|
44134
|
-
When done, reply with one short line summarising what you added.`;
|
|
44514
|
+
return resolveCliPrompt("cli.autonomous.intake", {
|
|
44515
|
+
booksIntro: BOOKS_INTRO,
|
|
44516
|
+
sMaxFiles: h.sMaxFiles,
|
|
44517
|
+
sMaxLines: h.sMaxLines,
|
|
44518
|
+
mMaxFiles: h.mMaxFiles,
|
|
44519
|
+
mMaxLines: h.mMaxLines
|
|
44520
|
+
});
|
|
44135
44521
|
}
|
|
44136
44522
|
function buildImplementPrompt(mine, subs, resumed) {
|
|
44137
44523
|
const subLines = subs.length ? subs.map((s3) => `- submodule \`${s3.dir}\` is on branch \`${s3.branch}\` \u2014 commit changes to it INSIDE \`${s3.dir}\``).join("\n") : "- (no submodules)";
|
|
44138
|
-
|
|
44139
|
-
|
|
44140
|
-
|
|
44141
|
-
|
|
44142
|
-
|
|
44143
|
-
|
|
44144
|
-
|
|
44145
|
-
|
|
44146
|
-
|
|
44147
|
-
|
|
44148
|
-
|
|
44149
|
-
- Follow the project's architecture and \`CLAUDE.md\`. Add/update tests; run the project's test command and WAIT for the result.
|
|
44150
|
-
- Commit your work (\`git add\` + \`git commit\`) in the root and/or inside each submodule you changed.
|
|
44151
|
-
- Do NOT push, do NOT open pull requests, do NOT switch/create branches, do NOT edit the book files
|
|
44152
|
-
(USER_TODO/USER_QA/AI_TODO/AI_PROGRESS/AI_DONE) \u2014 the 4PM cli delivers and records everything.
|
|
44153
|
-
- If you need a human DECISION to continue (ambiguous requirement, missing information, a choice with real
|
|
44154
|
-
trade-offs), do NOT guess: commit what you have and report \`needs-input\` with the question + options.
|
|
44155
|
-
|
|
44156
|
-
Finish with ONLY one fenced \`json\` block:
|
|
44157
|
-
{"status": "done" | "needs-input" | "failed", "summary": "<what you did / why it failed>", "question": "<only for needs-input: the question and the options>"}`;
|
|
44524
|
+
const resumedNote = resumed ? "\nThis is a CONTINUATION: earlier work for this task is already committed on the branches below \u2014 review it (git log / git diff against the base) and continue from it; do not start over.\n" : "";
|
|
44525
|
+
return resolveCliPrompt("cli.autonomous.implement", {
|
|
44526
|
+
taskId: mine.id,
|
|
44527
|
+
group: mine.task.group,
|
|
44528
|
+
attempt: mine.attempt,
|
|
44529
|
+
desc: mine.task.desc,
|
|
44530
|
+
notes: mine.task.notes || "(none)",
|
|
44531
|
+
resumedNote,
|
|
44532
|
+
branch: mine.branch,
|
|
44533
|
+
subLines
|
|
44534
|
+
});
|
|
44158
44535
|
}
|
|
44159
44536
|
function buildSplitPrompt(mine, reasons, diff2) {
|
|
44160
|
-
return
|
|
44161
|
-
|
|
44162
|
-
|
|
44163
|
-
|
|
44164
|
-
|
|
44165
|
-
|
|
44166
|
-
|
|
44167
|
-
|
|
44168
|
-
\`\`\`
|
|
44169
|
-
${diff2 || "(no changes yet)"}
|
|
44170
|
-
\`\`\`
|
|
44171
|
-
The first child continues from that WIP branch: note which parts are already done so they are not redone.
|
|
44172
|
-
Each child description must state its acceptance criteria and how to test it.
|
|
44173
|
-
|
|
44174
|
-
Reply with ONLY one fenced \`json\` block:
|
|
44175
|
-
{"children": [{"desc": "<task + acceptance criteria>", "priority": "High|Medium|Low", "size": "S|M", "notes": "<optional>"}]}`;
|
|
44537
|
+
return resolveCliPrompt("cli.autonomous.split", {
|
|
44538
|
+
taskId: mine.id,
|
|
44539
|
+
desc: mine.task.desc,
|
|
44540
|
+
notes: mine.task.notes || "(none)",
|
|
44541
|
+
reasons: reasons || "(unknown)",
|
|
44542
|
+
branch: mine.branch,
|
|
44543
|
+
diff: diff2 || "(no changes yet)"
|
|
44544
|
+
});
|
|
44176
44545
|
}
|
|
44177
44546
|
function parseJsonReply(text) {
|
|
44178
44547
|
const fenced = [...text.matchAll(/```json\s*([\s\S]*?)```/gi)].map((m2) => m2[1].trim());
|
|
@@ -45587,8 +45956,256 @@ function applyGitAuth(ctx, method, host) {
|
|
|
45587
45956
|
);
|
|
45588
45957
|
}
|
|
45589
45958
|
|
|
45590
|
-
// src/core/
|
|
45959
|
+
// src/core/research-run.ts
|
|
45591
45960
|
import { spawn as spawn8 } from "child_process";
|
|
45961
|
+
import { mkdtempSync as mkdtempSync3, rmSync as rmSync8, writeFileSync as writeFileSync9 } from "fs";
|
|
45962
|
+
import { tmpdir as tmpdir5 } from "os";
|
|
45963
|
+
import { join as join29 } from "path";
|
|
45964
|
+
var RESEARCH_TIMEOUT_MS = 9 * 60 * 1e3;
|
|
45965
|
+
var RESEARCH_MAX_TURNS = 16;
|
|
45966
|
+
var NO_USAGE2 = { tokens: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 };
|
|
45967
|
+
var RESEARCH_ALLOWED_TOOLS = "WebSearch,WebFetch";
|
|
45968
|
+
var RESEARCH_ATTACH_TOOLS = "WebSearch,WebFetch,Read,Glob";
|
|
45969
|
+
var RESEARCH_DENIED_BASE = [
|
|
45970
|
+
"Bash",
|
|
45971
|
+
"Edit",
|
|
45972
|
+
"Write",
|
|
45973
|
+
"Read",
|
|
45974
|
+
"Glob",
|
|
45975
|
+
"Grep",
|
|
45976
|
+
"NotebookEdit",
|
|
45977
|
+
"Task",
|
|
45978
|
+
"SlashCommand",
|
|
45979
|
+
"Skill",
|
|
45980
|
+
"ToolSearch",
|
|
45981
|
+
"TaskCreate",
|
|
45982
|
+
"TaskGet",
|
|
45983
|
+
"TaskList",
|
|
45984
|
+
"TaskOutput",
|
|
45985
|
+
"TaskStop",
|
|
45986
|
+
"TaskUpdate",
|
|
45987
|
+
"Monitor",
|
|
45988
|
+
"DesignSync",
|
|
45989
|
+
"CronCreate",
|
|
45990
|
+
"CronDelete",
|
|
45991
|
+
"CronList",
|
|
45992
|
+
"EnterWorktree",
|
|
45993
|
+
"ExitWorktree",
|
|
45994
|
+
"RemoteTrigger",
|
|
45995
|
+
"ScheduleWakeup",
|
|
45996
|
+
"SendMessage",
|
|
45997
|
+
"PushNotification"
|
|
45998
|
+
];
|
|
45999
|
+
function safeAttachmentName(name, index) {
|
|
46000
|
+
const base = (name || `file-${index + 1}`).replace(/[/\\]/g, "_").replace(/[^A-Za-z0-9._-]/g, "_").slice(0, 80);
|
|
46001
|
+
return base || `file-${index + 1}`;
|
|
46002
|
+
}
|
|
46003
|
+
function buildResearchPrompt(question) {
|
|
46004
|
+
return resolveCliPrompt("cli.research.policy", { question });
|
|
46005
|
+
}
|
|
46006
|
+
function runClaudeOnce2(cmd, profile, prompt, extraEnv, onChunk, denyDirs, attachments) {
|
|
46007
|
+
return new Promise((resolve13, reject) => {
|
|
46008
|
+
const isClaude = cmd.includes("claude");
|
|
46009
|
+
const cwd2 = mkdtempSync3(join29(tmpdir5(), "4pm-research-"));
|
|
46010
|
+
const hasAttachments = attachments.length > 0;
|
|
46011
|
+
let finalPrompt = prompt;
|
|
46012
|
+
if (hasAttachments) {
|
|
46013
|
+
const lines = [];
|
|
46014
|
+
attachments.forEach((a, i) => {
|
|
46015
|
+
const file = join29(cwd2, safeAttachmentName(a.name, i));
|
|
46016
|
+
try {
|
|
46017
|
+
writeFileSync9(file, Buffer.from(a.dataBase64, "base64"));
|
|
46018
|
+
lines.push(`- \`${file}\``);
|
|
46019
|
+
} catch {
|
|
46020
|
+
}
|
|
46021
|
+
});
|
|
46022
|
+
if (lines.length > 0) {
|
|
46023
|
+
finalPrompt = `${prompt}
|
|
46024
|
+
|
|
46025
|
+
The user attached these files \u2014 use the Read tool to read them before answering:
|
|
46026
|
+
${lines.join("\n")}`;
|
|
46027
|
+
}
|
|
46028
|
+
}
|
|
46029
|
+
const args = [
|
|
46030
|
+
"-p",
|
|
46031
|
+
...profile?.model ? ["--model", profile.model] : [],
|
|
46032
|
+
...isClaude ? ["--output-format", "stream-json", "--verbose"] : [],
|
|
46033
|
+
// Secret-path deny rules (ADR-0347) — reads are not confined to the working dir.
|
|
46034
|
+
...denySettingsArgs(cmd, [...denyDirs, profile?.dir]),
|
|
46035
|
+
// Web-only agent (+ Read/Glob of the scratch dir when attachments are present): allow the tool set,
|
|
46036
|
+
// deny everything else, bound the loop. `--permission-mode default` is the non-variadic terminator
|
|
46037
|
+
// (so the variadic tool flags consume only their own comma-token — same trick as the other runners).
|
|
46038
|
+
...isClaude ? [
|
|
46039
|
+
`--max-turns=${RESEARCH_MAX_TURNS}`,
|
|
46040
|
+
"--allowedTools",
|
|
46041
|
+
hasAttachments ? RESEARCH_ATTACH_TOOLS : RESEARCH_ALLOWED_TOOLS,
|
|
46042
|
+
"--disallowedTools",
|
|
46043
|
+
(hasAttachments ? RESEARCH_DENIED_BASE.filter((t2) => t2 !== "Read" && t2 !== "Glob") : RESEARCH_DENIED_BASE).join(","),
|
|
46044
|
+
"--permission-mode",
|
|
46045
|
+
"default"
|
|
46046
|
+
] : []
|
|
46047
|
+
];
|
|
46048
|
+
const env3 = agentEnv(extraEnv, profile ? { CLAUDE_CONFIG_DIR: profile.dir } : void 0);
|
|
46049
|
+
const cleanup = () => {
|
|
46050
|
+
try {
|
|
46051
|
+
rmSync8(cwd2, { recursive: true, force: true });
|
|
46052
|
+
} catch {
|
|
46053
|
+
}
|
|
46054
|
+
};
|
|
46055
|
+
const child = spawn8(cmd, args, { stdio: ["pipe", "pipe", "pipe"], env: env3, cwd: cwd2 });
|
|
46056
|
+
const parser = createAiStreamParser(cmd);
|
|
46057
|
+
let out = "";
|
|
46058
|
+
let err = "";
|
|
46059
|
+
const timer = setTimeout(() => {
|
|
46060
|
+
child.kill("SIGKILL");
|
|
46061
|
+
reject(new Error("research run timed out"));
|
|
46062
|
+
}, RESEARCH_TIMEOUT_MS);
|
|
46063
|
+
timer.unref();
|
|
46064
|
+
child.stdout.on("data", (d) => {
|
|
46065
|
+
const delta = parser.push(d.toString());
|
|
46066
|
+
if (delta) {
|
|
46067
|
+
out += delta;
|
|
46068
|
+
onChunk(delta);
|
|
46069
|
+
}
|
|
46070
|
+
});
|
|
46071
|
+
child.stderr.on("data", (d) => err += d.toString());
|
|
46072
|
+
child.on("error", (e) => {
|
|
46073
|
+
clearTimeout(timer);
|
|
46074
|
+
cleanup();
|
|
46075
|
+
reject(e);
|
|
46076
|
+
});
|
|
46077
|
+
child.on("close", (code) => {
|
|
46078
|
+
clearTimeout(timer);
|
|
46079
|
+
cleanup();
|
|
46080
|
+
const tail = parser.flush();
|
|
46081
|
+
if (tail) {
|
|
46082
|
+
out += tail;
|
|
46083
|
+
onChunk(tail);
|
|
46084
|
+
}
|
|
46085
|
+
resolve13({ code: code ?? -1, out, err, usage: parser.usage() });
|
|
46086
|
+
});
|
|
46087
|
+
child.stdin.write(finalPrompt);
|
|
46088
|
+
child.stdin.end();
|
|
46089
|
+
});
|
|
46090
|
+
}
|
|
46091
|
+
async function runResearch(question, ai2, onChunk, attachments = []) {
|
|
46092
|
+
const prompt = buildResearchPrompt(question);
|
|
46093
|
+
const denyDirs = ai2.profiles.map((p2) => p2.dir);
|
|
46094
|
+
const attempts = ai2.profiles.length > 0 ? ai2.profiles : [null];
|
|
46095
|
+
let lastReason = "no attempt";
|
|
46096
|
+
for (let i = 0; i < attempts.length; i++) {
|
|
46097
|
+
try {
|
|
46098
|
+
const { code, out, err, usage } = await runClaudeOnce2(ai2.cmd, attempts[i], prompt, ai2.env, onChunk, denyDirs, attachments);
|
|
46099
|
+
if (code === 0 && out.trim()) {
|
|
46100
|
+
const u2 = usage.tokens > 0 ? usage : { ...NO_USAGE2, tokens: estimateTokens(out) };
|
|
46101
|
+
return { text: out.trim(), usage: u2 };
|
|
46102
|
+
}
|
|
46103
|
+
lastReason = `exited ${code}: ${(err || out).slice(0, 300)}`;
|
|
46104
|
+
} catch (e) {
|
|
46105
|
+
lastReason = e instanceof Error ? e.message : String(e);
|
|
46106
|
+
}
|
|
46107
|
+
}
|
|
46108
|
+
return { text: "", usage: { ...NO_USAGE2 }, error: lastReason };
|
|
46109
|
+
}
|
|
46110
|
+
|
|
46111
|
+
// src/core/research-guard.ts
|
|
46112
|
+
var REFUSAL_PREFIX = "REFUSED:";
|
|
46113
|
+
var ATTACK_INTENT = [
|
|
46114
|
+
/\b(hack|break|breare|breach|compromise|exploit|gain (unauthorized )?access to|take over|take control of|ddos|dos attack)\b[^.?!]{0,60}\b(into|someone|their|his|her|another|the victim'?s|a target|account|server|network|website|wifi|camera|phone|system|database)\b/i,
|
|
46115
|
+
/\b(write|create|build|generate|give me)\b[^.?!]{0,40}\b(malware|ransomware|a virus|a keylogger|a trojan|spyware|a rootkit|a worm|a botnet|an exploit|a phishing (kit|page|email|site))\b/i,
|
|
46116
|
+
/\bbypass\b[^.?!]{0,40}\b(authentication|login|the firewall|a paywall|security|2fa|mfa|licen[sc]e|drm)\b/i,
|
|
46117
|
+
/\b(steal|harvest|exfiltrate|dump|crack)\b[^.?!]{0,40}\b(credentials|passwords|password hashes|credit cards?|personal data|pii|session tokens)\b/i,
|
|
46118
|
+
/\bbrute[- ]?force\b[^.?!]{0,40}\b(login|password|account|credential)/i
|
|
46119
|
+
];
|
|
46120
|
+
function isModelRefusal(text) {
|
|
46121
|
+
return text.trimStart().toUpperCase().startsWith(REFUSAL_PREFIX);
|
|
46122
|
+
}
|
|
46123
|
+
function refusalReasonOf(text) {
|
|
46124
|
+
const after = text.trim().slice(REFUSAL_PREFIX.length).trim();
|
|
46125
|
+
return (after || "This question is outside what Research can answer.").slice(0, 300);
|
|
46126
|
+
}
|
|
46127
|
+
function screenQuestion(question) {
|
|
46128
|
+
if (ATTACK_INTENT.some((re2) => re2.test(question))) {
|
|
46129
|
+
return { blocked: true, reason: "The question asks to attack or compromise a system, which Research does not do." };
|
|
46130
|
+
}
|
|
46131
|
+
return { blocked: false };
|
|
46132
|
+
}
|
|
46133
|
+
function screenAnswer(answer) {
|
|
46134
|
+
if (isModelRefusal(answer)) return { blocked: true, reason: refusalReasonOf(answer) };
|
|
46135
|
+
if (ATTACK_INTENT.some((re2) => re2.test(answer))) {
|
|
46136
|
+
return { blocked: true, reason: "The answer was withheld by the content policy." };
|
|
46137
|
+
}
|
|
46138
|
+
return { blocked: false };
|
|
46139
|
+
}
|
|
46140
|
+
|
|
46141
|
+
// src/core/ws-client/handlers/research.ts
|
|
46142
|
+
function handleResearchChannels(ctx, message, payload) {
|
|
46143
|
+
if (message.channel !== WsChannels.RESEARCH_ASK) return false;
|
|
46144
|
+
const req = payload;
|
|
46145
|
+
const config = readProfileConfig(ctx.profileDir);
|
|
46146
|
+
const cmd = config.aiCli || "claude";
|
|
46147
|
+
ctx.bus.push({ source: "server", kind: "aireq", text: `${cmd} \u2039 research` });
|
|
46148
|
+
ctx.bus.startBusy("research");
|
|
46149
|
+
void run14(ctx, message, req, config, cmd).finally(() => ctx.bus.endBusy("research"));
|
|
46150
|
+
return true;
|
|
46151
|
+
}
|
|
46152
|
+
async function run14(ctx, message, req, config, cmd) {
|
|
46153
|
+
const emit = (p2) => ctx.send(WsChannels.RESEARCH_PROGRESS, { queryId: req.queryId, ...p2 });
|
|
46154
|
+
const reply = (r) => ctx.send(WsChannels.RESEARCH_ASK, r, message.id);
|
|
46155
|
+
const pre = screenQuestion(req.question);
|
|
46156
|
+
if (pre.blocked) {
|
|
46157
|
+
reply({ refused: true, refusalReason: pre.reason });
|
|
46158
|
+
return;
|
|
46159
|
+
}
|
|
46160
|
+
const slot = await acquireRunSlot(ctx, (info) => emit({ message: `queued #${info.position}` }));
|
|
46161
|
+
if (slot.kind !== "granted") {
|
|
46162
|
+
reply({ error: slot.kind === "denied" ? "storage full" : "no idle run slot" });
|
|
46163
|
+
return;
|
|
46164
|
+
}
|
|
46165
|
+
try {
|
|
46166
|
+
const ai2 = {
|
|
46167
|
+
cmd,
|
|
46168
|
+
profiles: resolveClaudeProfiles(config, getWorkingProfile(ctx.profileDir, cmd)),
|
|
46169
|
+
env: config.aiEnv
|
|
46170
|
+
};
|
|
46171
|
+
const result = await runResearch(req.question, ai2, (delta) => emit({ chunk: delta }), req.attachments ?? []);
|
|
46172
|
+
const occurredAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
46173
|
+
const events = [{ metric: UsageMetric.COMMANDS, amount: 1, occurredAt }];
|
|
46174
|
+
if (result.usage.tokens > 0) {
|
|
46175
|
+
events.push({
|
|
46176
|
+
metric: UsageMetric.AI_TOKENS,
|
|
46177
|
+
amount: result.usage.tokens,
|
|
46178
|
+
occurredAt,
|
|
46179
|
+
inputTokens: result.usage.input,
|
|
46180
|
+
outputTokens: result.usage.output,
|
|
46181
|
+
cacheReadTokens: result.usage.cacheRead,
|
|
46182
|
+
cacheCreationTokens: result.usage.cacheCreation,
|
|
46183
|
+
provider: aiProviderOf(cmd),
|
|
46184
|
+
authMode: resolveClaudeAuthMode(config)
|
|
46185
|
+
});
|
|
46186
|
+
}
|
|
46187
|
+
ctx.reportUsage(events, null);
|
|
46188
|
+
if (result.error || !result.text) {
|
|
46189
|
+
ctx.bus.push({ source: "server", kind: "log", text: `research failed: ${result.error ?? "empty"}`, level: "warn" });
|
|
46190
|
+
reply({ error: result.error || "empty answer", tokens: result.usage.tokens });
|
|
46191
|
+
return;
|
|
46192
|
+
}
|
|
46193
|
+
const post2 = screenAnswer(result.text);
|
|
46194
|
+
if (post2.blocked) {
|
|
46195
|
+
reply({ refused: true, refusalReason: post2.reason, tokens: result.usage.tokens });
|
|
46196
|
+
return;
|
|
46197
|
+
}
|
|
46198
|
+
ctx.bus.push({ source: "server", kind: "aires", text: `${cmd} \u203A` });
|
|
46199
|
+
reply({ answer: result.text, tokens: result.usage.tokens });
|
|
46200
|
+
} catch (err) {
|
|
46201
|
+
reply({ error: err instanceof Error ? err.message : String(err) });
|
|
46202
|
+
} finally {
|
|
46203
|
+
slot.handle.release();
|
|
46204
|
+
}
|
|
46205
|
+
}
|
|
46206
|
+
|
|
46207
|
+
// src/core/knowledge-compose.ts
|
|
46208
|
+
import { spawn as spawn9 } from "child_process";
|
|
45592
46209
|
var COMPOSE_TIMEOUT_MS = 18e4;
|
|
45593
46210
|
function buildPrompt3(req) {
|
|
45594
46211
|
const focus = req.prompt ? `
|
|
@@ -45614,7 +46231,7 @@ function runOnce2(cmd, profile, prompt, cwd2, extraEnv, denyDirs = []) {
|
|
|
45614
46231
|
...extraEnv,
|
|
45615
46232
|
...profile ? { CLAUDE_CONFIG_DIR: profile.dir } : {}
|
|
45616
46233
|
};
|
|
45617
|
-
const child =
|
|
46234
|
+
const child = spawn9(cmd, args, { stdio: ["pipe", "pipe", "pipe"], cwd: cwd2, env: env3 });
|
|
45618
46235
|
let out = "";
|
|
45619
46236
|
let err = "";
|
|
45620
46237
|
const timer = setTimeout(() => {
|
|
@@ -45660,14 +46277,14 @@ async function runKnowledgeCompose(req, ai2, physicRoot) {
|
|
|
45660
46277
|
}
|
|
45661
46278
|
|
|
45662
46279
|
// src/core/faq-compose.ts
|
|
45663
|
-
import { spawn as
|
|
45664
|
-
import { existsSync as existsSync16, mkdirSync as mkdirSync8, rmSync as
|
|
46280
|
+
import { spawn as spawn10, execFile as execFile14 } from "child_process";
|
|
46281
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync8, rmSync as rmSync9 } from "fs";
|
|
45665
46282
|
import { randomUUID as randomUUID3 } from "crypto";
|
|
45666
|
-
import { join as
|
|
45667
|
-
import { tmpdir as
|
|
46283
|
+
import { join as join30 } from "path";
|
|
46284
|
+
import { tmpdir as tmpdir6 } from "os";
|
|
45668
46285
|
import { promisify as promisify14 } from "util";
|
|
45669
46286
|
var execFileAsync2 = promisify14(execFile14);
|
|
45670
|
-
var
|
|
46287
|
+
var NO_USAGE3 = { tokens: 0, input: 0, output: 0, cacheRead: 0, cacheCreation: 0 };
|
|
45671
46288
|
var COMPOSE_TIMEOUT_MS2 = 10 * 60 * 1e3;
|
|
45672
46289
|
var AGENT_DENIED_TOOLS = ["Bash", "WebFetch", "WebSearch", "NotebookEdit", "Task"];
|
|
45673
46290
|
function buildPrompt4(tickets, customPrompt) {
|
|
@@ -45677,33 +46294,12 @@ function buildPrompt4(tickets, customPrompt) {
|
|
|
45677
46294
|
${thread}`;
|
|
45678
46295
|
}).join("\n\n");
|
|
45679
46296
|
const ids = tickets.map((t2) => t2.id);
|
|
45680
|
-
|
|
45681
|
-
|
|
45682
|
-
|
|
45683
|
-
|
|
45684
|
-
|
|
45685
|
-
|
|
45686
|
-
"Rules:",
|
|
45687
|
-
"- Write generalized Q&A, NOT ticket-specific replies; never include customer names, emails, ids,",
|
|
45688
|
-
" org names or any personal data from the tickets.",
|
|
45689
|
-
"- Merge with existing entries instead of duplicating; keep the existing formatting and headings.",
|
|
45690
|
-
"- If a ticket yields no reusable FAQ value, skip it. If nothing is worth adding, make NO changes.",
|
|
45691
|
-
"- Only edit markdown documentation files; do not touch code, CI, or unrelated files.",
|
|
45692
|
-
"- Do NOT run git commit/push or open a PR yourself \u2014 just leave the edited files in the working",
|
|
45693
|
-
" tree; the surrounding tooling handles commit + PR.",
|
|
45694
|
-
...customPrompt?.trim() ? ["", "Additional instruction from the operator (follow it too):", customPrompt.trim()] : [],
|
|
45695
|
-
"",
|
|
45696
|
-
"===== RESOLVED TICKETS =====",
|
|
45697
|
-
transcript,
|
|
45698
|
-
"",
|
|
45699
|
-
"===== REQUIRED FINAL OUTPUT =====",
|
|
45700
|
-
"After finishing all file edits, output \u2014 as the LAST thing, on its own \u2014 a single fenced JSON",
|
|
45701
|
-
"block summarizing what you did PER TICKET (created/updated which FAQ entry, or skipped + why):",
|
|
45702
|
-
"```json",
|
|
45703
|
-
`{"perTicket":[${ids.map((id) => `{"ticketId":"${id}","summary":"..."}`).join(",")}]}`,
|
|
45704
|
-
"```",
|
|
45705
|
-
"Include exactly one entry per ticket id above; keep each summary to one or two sentences."
|
|
45706
|
-
].join("\n");
|
|
46297
|
+
const ticketJsonSkeleton = `{"perTicket":[${ids.map((id) => `{"ticketId":"${id}","summary":"..."}`).join(",")}]}`;
|
|
46298
|
+
const customInstruction = customPrompt?.trim() ? `
|
|
46299
|
+
|
|
46300
|
+
Additional instruction from the operator (follow it too):
|
|
46301
|
+
${customPrompt.trim()}` : "";
|
|
46302
|
+
return resolveCliPrompt("cli.faq.compose", { transcript, ticketJsonSkeleton, customInstruction });
|
|
45707
46303
|
}
|
|
45708
46304
|
function parsePerTicket(text) {
|
|
45709
46305
|
const idx = text.lastIndexOf('"perTicket"');
|
|
@@ -45730,7 +46326,7 @@ function parsePerTicket(text) {
|
|
|
45730
46326
|
}
|
|
45731
46327
|
return [];
|
|
45732
46328
|
}
|
|
45733
|
-
function
|
|
46329
|
+
function runClaudeOnce3(cmd, profile, prompt, cwd2, extraEnv, denyDirs = []) {
|
|
45734
46330
|
return new Promise((resolve13, reject) => {
|
|
45735
46331
|
const isClaude = cmd.includes("claude");
|
|
45736
46332
|
const args = [
|
|
@@ -45746,7 +46342,7 @@ function runClaudeOnce2(cmd, profile, prompt, cwd2, extraEnv, denyDirs = []) {
|
|
|
45746
46342
|
...cmd.includes("codex") ? ["--sandbox", "workspace-write"] : []
|
|
45747
46343
|
];
|
|
45748
46344
|
const env3 = agentEnv(extraEnv, profile ? { CLAUDE_CONFIG_DIR: profile.dir } : void 0);
|
|
45749
|
-
const child =
|
|
46345
|
+
const child = spawn10(cmd, args, { stdio: ["pipe", "pipe", "pipe"], cwd: cwd2, env: env3 });
|
|
45750
46346
|
const parser = createAiStreamParser(cmd);
|
|
45751
46347
|
let out = "";
|
|
45752
46348
|
let err = "";
|
|
@@ -45774,7 +46370,7 @@ async function runClaudeWithFailover2(prompt, cwd2, ai2) {
|
|
|
45774
46370
|
const attempts = ai2.profiles.length > 0 ? ai2.profiles : [null];
|
|
45775
46371
|
let lastReason = "no attempt";
|
|
45776
46372
|
for (let i = 0; i < attempts.length; i++) {
|
|
45777
|
-
const { code, out, err, usage } = await
|
|
46373
|
+
const { code, out, err, usage } = await runClaudeOnce3(ai2.cmd, attempts[i], prompt, cwd2, ai2.env, ai2.profiles.map((p2) => p2.dir));
|
|
45778
46374
|
if (code === 0) return { usage, text: out.trim() };
|
|
45779
46375
|
lastReason = `exited ${code}: ${(err || out).slice(0, 500)}`;
|
|
45780
46376
|
if (i === attempts.length - 1) break;
|
|
@@ -45784,7 +46380,7 @@ async function runClaudeWithFailover2(prompt, cwd2, ai2) {
|
|
|
45784
46380
|
var MAX_OUTPUT_CHARS = 8e3;
|
|
45785
46381
|
async function runFaqCompose(req, ai2) {
|
|
45786
46382
|
if (!req.repo.token) return { prUrl: "", error: "no write token" };
|
|
45787
|
-
const workDir =
|
|
46383
|
+
const workDir = join30(tmpdir6(), "4pm-faq-sync", randomUUID3());
|
|
45788
46384
|
const gitEnv = { ...process.env, GIT_TERMINAL_PROMPT: "0" };
|
|
45789
46385
|
const auth = gitAuthArgs("x-access-token", req.repo.token);
|
|
45790
46386
|
try {
|
|
@@ -45814,7 +46410,7 @@ async function runFaqCompose(req, ai2) {
|
|
|
45814
46410
|
} catch (err) {
|
|
45815
46411
|
logger.warn("faq.compose.pr.failed", { error: String(err) });
|
|
45816
46412
|
}
|
|
45817
|
-
const u2 = usage.tokens > 0 ? usage : { ...
|
|
46413
|
+
const u2 = usage.tokens > 0 ? usage : { ...NO_USAGE3 };
|
|
45818
46414
|
return {
|
|
45819
46415
|
prUrl,
|
|
45820
46416
|
branch: req.headBranch,
|
|
@@ -45830,7 +46426,7 @@ async function runFaqCompose(req, ai2) {
|
|
|
45830
46426
|
} finally {
|
|
45831
46427
|
if (existsSync16(workDir)) {
|
|
45832
46428
|
try {
|
|
45833
|
-
|
|
46429
|
+
rmSync9(workDir, { recursive: true, force: true });
|
|
45834
46430
|
} catch {
|
|
45835
46431
|
}
|
|
45836
46432
|
}
|
|
@@ -45926,15 +46522,45 @@ function handleSupportChannels(ctx, message, payload) {
|
|
|
45926
46522
|
};
|
|
45927
46523
|
ctx.bus.push({ source: "server", kind: "aireq", text: `${ccmd} \u2039 author checklist items` });
|
|
45928
46524
|
ctx.bus.startBusy("checklist-author");
|
|
45929
|
-
void
|
|
45930
|
-
|
|
45931
|
-
|
|
45932
|
-
|
|
45933
|
-
|
|
46525
|
+
void (async () => {
|
|
46526
|
+
const slot = await acquireRunSlot(ctx, () => void 0);
|
|
46527
|
+
if (slot.kind !== "granted") {
|
|
46528
|
+
ctx.send(
|
|
46529
|
+
WsChannels.CHECKLIST_AUTHOR,
|
|
46530
|
+
{ error: slot.kind === "denied" ? "storage full" : "no idle run slot" },
|
|
46531
|
+
message.id
|
|
46532
|
+
);
|
|
46533
|
+
return;
|
|
46534
|
+
}
|
|
46535
|
+
try {
|
|
46536
|
+
const { reply, usage } = await runChecklistAuthor(creq, cai);
|
|
46537
|
+
const occurredAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
46538
|
+
const events = [{ metric: UsageMetric.COMMANDS, amount: 1, occurredAt }];
|
|
46539
|
+
if (usage.tokens > 0) {
|
|
46540
|
+
events.push({
|
|
46541
|
+
metric: UsageMetric.AI_TOKENS,
|
|
46542
|
+
amount: usage.tokens,
|
|
46543
|
+
occurredAt,
|
|
46544
|
+
inputTokens: usage.input,
|
|
46545
|
+
outputTokens: usage.output,
|
|
46546
|
+
cacheReadTokens: usage.cacheRead,
|
|
46547
|
+
cacheCreationTokens: usage.cacheCreation,
|
|
46548
|
+
provider: aiProviderOf(ccmd),
|
|
46549
|
+
authMode: resolveClaudeAuthMode(ccfg)
|
|
46550
|
+
});
|
|
46551
|
+
}
|
|
46552
|
+
ctx.reportUsage(events, null);
|
|
46553
|
+
if (reply.error) {
|
|
46554
|
+
ctx.bus.push({ source: "server", kind: "log", text: `checklist-author failed: ${reply.error}`, level: "warn" });
|
|
46555
|
+
} else {
|
|
46556
|
+
ctx.bus.push({ source: "server", kind: "aires", text: `${ccmd} \u203A` });
|
|
46557
|
+
}
|
|
46558
|
+
reportToolResult(ccmd, !reply.error, reply.error);
|
|
46559
|
+
ctx.send(WsChannels.CHECKLIST_AUTHOR, reply, message.id);
|
|
46560
|
+
} finally {
|
|
46561
|
+
slot.handle.release();
|
|
45934
46562
|
}
|
|
45935
|
-
|
|
45936
|
-
ctx.send(WsChannels.CHECKLIST_AUTHOR, reply, message.id);
|
|
45937
|
-
}).finally(() => ctx.bus.endBusy("checklist-author"));
|
|
46563
|
+
})().finally(() => ctx.bus.endBusy("checklist-author"));
|
|
45938
46564
|
return true;
|
|
45939
46565
|
}
|
|
45940
46566
|
default:
|
|
@@ -46035,7 +46661,7 @@ function handleToolsChannels(ctx, message, payload) {
|
|
|
46035
46661
|
|
|
46036
46662
|
// src/core/agents.ts
|
|
46037
46663
|
import { readFile as readFile14, writeFile as writeFile10, mkdir as mkdir8, readdir as readdir6, rm as rm7 } from "fs/promises";
|
|
46038
|
-
import { join as
|
|
46664
|
+
import { join as join31 } from "path";
|
|
46039
46665
|
var AGENTS_REL = ".claude/agents";
|
|
46040
46666
|
var SKILLS_REL = ".claude/skills";
|
|
46041
46667
|
var MEMORY_DIRS = /* @__PURE__ */ new Set(["short-memory", "long-memory"]);
|
|
@@ -46059,17 +46685,17 @@ async function listAgents(root) {
|
|
|
46059
46685
|
const subagents = [];
|
|
46060
46686
|
const skills = [];
|
|
46061
46687
|
try {
|
|
46062
|
-
for (const e of await readdir6(
|
|
46688
|
+
for (const e of await readdir6(join31(root, AGENTS_REL), { withFileTypes: true })) {
|
|
46063
46689
|
if (!e.isFile() || !e.name.endsWith(".md") || MEMORY_DIRS.has(e.name)) continue;
|
|
46064
|
-
const fm = frontmatter(await readText2(
|
|
46690
|
+
const fm = frontmatter(await readText2(join31(root, AGENTS_REL, e.name)));
|
|
46065
46691
|
subagents.push({ name: e.name.replace(/\.md$/, ""), description: fm.description });
|
|
46066
46692
|
}
|
|
46067
46693
|
} catch {
|
|
46068
46694
|
}
|
|
46069
46695
|
try {
|
|
46070
|
-
for (const e of await readdir6(
|
|
46696
|
+
for (const e of await readdir6(join31(root, SKILLS_REL), { withFileTypes: true })) {
|
|
46071
46697
|
if (!e.isDirectory()) continue;
|
|
46072
|
-
const fm = frontmatter(await readText2(
|
|
46698
|
+
const fm = frontmatter(await readText2(join31(root, SKILLS_REL, e.name, "SKILL.md")));
|
|
46073
46699
|
skills.push({ name: e.name, description: fm.description });
|
|
46074
46700
|
}
|
|
46075
46701
|
} catch {
|
|
@@ -46080,15 +46706,15 @@ async function listAgents(root) {
|
|
|
46080
46706
|
}
|
|
46081
46707
|
function itemPath(root, kind, name) {
|
|
46082
46708
|
const n = safeName(name);
|
|
46083
|
-
return kind === "subagent" ?
|
|
46709
|
+
return kind === "subagent" ? join31(root, AGENTS_REL, `${n}.md`) : join31(root, SKILLS_REL, n, "SKILL.md");
|
|
46084
46710
|
}
|
|
46085
46711
|
async function readAgent(root, kind, name) {
|
|
46086
46712
|
const content = await readText2(itemPath(root, kind, name));
|
|
46087
46713
|
if (kind !== "subagent") return { content };
|
|
46088
46714
|
const n = safeName(name);
|
|
46089
46715
|
const [short, long] = await Promise.all([
|
|
46090
|
-
readText2(
|
|
46091
|
-
readText2(
|
|
46716
|
+
readText2(join31(root, AGENTS_REL, "short-memory", `${n}.md`)),
|
|
46717
|
+
readText2(join31(root, AGENTS_REL, "long-memory", `${n}.md`))
|
|
46092
46718
|
]);
|
|
46093
46719
|
return { content, memory: { short, long } };
|
|
46094
46720
|
}
|
|
@@ -46098,12 +46724,12 @@ async function writeAgent(root, req) {
|
|
|
46098
46724
|
try {
|
|
46099
46725
|
if (req.action === "delete") {
|
|
46100
46726
|
if (req.kind === "subagent") await rm7(itemPath(root, "subagent", n), { force: true });
|
|
46101
|
-
else await rm7(
|
|
46727
|
+
else await rm7(join31(root, SKILLS_REL, n), { recursive: true, force: true });
|
|
46102
46728
|
return { ok: true };
|
|
46103
46729
|
}
|
|
46104
46730
|
if (req.content === void 0) return { ok: false, error: "content is required" };
|
|
46105
46731
|
const path = itemPath(root, req.kind, n);
|
|
46106
|
-
await mkdir8(
|
|
46732
|
+
await mkdir8(join31(path, ".."), { recursive: true });
|
|
46107
46733
|
await writeFile10(path, req.content, "utf8");
|
|
46108
46734
|
return { ok: true };
|
|
46109
46735
|
} catch (err) {
|
|
@@ -46113,7 +46739,7 @@ async function writeAgent(root, req) {
|
|
|
46113
46739
|
|
|
46114
46740
|
// src/core/secrets.ts
|
|
46115
46741
|
import { readFile as readFile15, writeFile as writeFile11 } from "fs/promises";
|
|
46116
|
-
import { join as
|
|
46742
|
+
import { join as join32 } from "path";
|
|
46117
46743
|
var SECURITY_REL = "AI_SECURITY.md";
|
|
46118
46744
|
var PLACEHOLDER_REL = "AI_PLACEHOLDER.md";
|
|
46119
46745
|
var SECRETS_REL = "project.secrets.json";
|
|
@@ -46126,7 +46752,7 @@ async function readText3(path, fallback = "") {
|
|
|
46126
46752
|
}
|
|
46127
46753
|
async function readSecretsObject(root) {
|
|
46128
46754
|
try {
|
|
46129
|
-
const v2 = JSON.parse(await readFile15(
|
|
46755
|
+
const v2 = JSON.parse(await readFile15(join32(root, SECRETS_REL), "utf8"));
|
|
46130
46756
|
return v2 && typeof v2 === "object" ? v2 : {};
|
|
46131
46757
|
} catch {
|
|
46132
46758
|
return {};
|
|
@@ -46145,8 +46771,8 @@ function placeholderKeys(text) {
|
|
|
46145
46771
|
}
|
|
46146
46772
|
async function readSecrets(root) {
|
|
46147
46773
|
const [security, placeholder, secrets] = await Promise.all([
|
|
46148
|
-
readText3(
|
|
46149
|
-
readText3(
|
|
46774
|
+
readText3(join32(root, SECURITY_REL)),
|
|
46775
|
+
readText3(join32(root, PLACEHOLDER_REL)),
|
|
46150
46776
|
readSecretsObject(root)
|
|
46151
46777
|
]);
|
|
46152
46778
|
const names = /* @__PURE__ */ new Set([...placeholderKeys(placeholder), ...Object.keys(secrets)]);
|
|
@@ -46159,14 +46785,14 @@ async function readSecrets(root) {
|
|
|
46159
46785
|
async function writeSecrets(root, req) {
|
|
46160
46786
|
try {
|
|
46161
46787
|
if (req.kind === "security") {
|
|
46162
|
-
await writeFile11(
|
|
46788
|
+
await writeFile11(join32(root, SECURITY_REL), req.content, "utf8");
|
|
46163
46789
|
} else if (req.kind === "placeholder") {
|
|
46164
|
-
await writeFile11(
|
|
46790
|
+
await writeFile11(join32(root, PLACEHOLDER_REL), req.content, "utf8");
|
|
46165
46791
|
} else {
|
|
46166
46792
|
const secrets = await readSecretsObject(root);
|
|
46167
46793
|
if (req.kind === "secret") secrets[req.key] = req.value;
|
|
46168
46794
|
else delete secrets[req.key];
|
|
46169
|
-
await writeFile11(
|
|
46795
|
+
await writeFile11(join32(root, SECRETS_REL), JSON.stringify(secrets, null, 2) + "\n", "utf8");
|
|
46170
46796
|
}
|
|
46171
46797
|
return { ok: true };
|
|
46172
46798
|
} catch (err) {
|
|
@@ -46176,12 +46802,12 @@ async function writeSecrets(root, req) {
|
|
|
46176
46802
|
|
|
46177
46803
|
// src/core/agent-tools.ts
|
|
46178
46804
|
import { readFile as readFile16, writeFile as writeFile12, mkdir as mkdir9 } from "fs/promises";
|
|
46179
|
-
import { dirname as dirname9, join as
|
|
46805
|
+
import { dirname as dirname9, join as join33 } from "path";
|
|
46180
46806
|
var SHARED_REL = ".claude/settings.json";
|
|
46181
46807
|
var LOCAL_REL = ".claude/settings.local.json";
|
|
46182
46808
|
var SECRETS_DENY = "Read(./project.secrets.json)";
|
|
46183
46809
|
function fileFor(root, scope) {
|
|
46184
|
-
return
|
|
46810
|
+
return join33(root, scope === "shared" ? SHARED_REL : LOCAL_REL);
|
|
46185
46811
|
}
|
|
46186
46812
|
async function readSettingsObject(path) {
|
|
46187
46813
|
try {
|
|
@@ -46218,7 +46844,7 @@ async function writeAgentTools(root, scope, permissions) {
|
|
|
46218
46844
|
// src/core/packages.ts
|
|
46219
46845
|
import { createHash as createHash4 } from "crypto";
|
|
46220
46846
|
import { readFile as readFile17, writeFile as writeFile13, mkdir as mkdir10, readdir as readdir7, rm as rm8, stat as stat5 } from "fs/promises";
|
|
46221
|
-
import { dirname as dirname10, join as
|
|
46847
|
+
import { dirname as dirname10, join as join34, relative } from "path";
|
|
46222
46848
|
var AGENTS_REL2 = ".claude/agents";
|
|
46223
46849
|
var SKILLS_REL2 = ".claude/skills";
|
|
46224
46850
|
var MANIFEST_REL = ".claude/.4pm-packages.json";
|
|
@@ -46242,7 +46868,7 @@ async function listFilesRel(dir2) {
|
|
|
46242
46868
|
return;
|
|
46243
46869
|
}
|
|
46244
46870
|
for (const e of entries2) {
|
|
46245
|
-
const full =
|
|
46871
|
+
const full = join34(d, e.name);
|
|
46246
46872
|
if (e.isDirectory()) await walk2(full);
|
|
46247
46873
|
else if (e.isFile()) out.push(relative(dir2, full));
|
|
46248
46874
|
}
|
|
@@ -46253,16 +46879,16 @@ async function listFilesRel(dir2) {
|
|
|
46253
46879
|
async function readArtifactFiles(root, kind, slug) {
|
|
46254
46880
|
const files = [];
|
|
46255
46881
|
if (kind === "subagent") {
|
|
46256
|
-
const agent = await readMaybe(
|
|
46882
|
+
const agent = await readMaybe(join34(root, AGENTS_REL2, `${slug}.md`));
|
|
46257
46883
|
if (agent) files.push({ path: "agent.md", contentBase64: agent.toString("base64") });
|
|
46258
|
-
const short = await readMaybe(
|
|
46884
|
+
const short = await readMaybe(join34(root, AGENTS_REL2, "short-memory", `${slug}.md`));
|
|
46259
46885
|
if (short) files.push({ path: "short-memory.md", contentBase64: short.toString("base64") });
|
|
46260
|
-
const long = await readMaybe(
|
|
46886
|
+
const long = await readMaybe(join34(root, AGENTS_REL2, "long-memory", `${slug}.md`));
|
|
46261
46887
|
if (long) files.push({ path: "long-memory.md", contentBase64: long.toString("base64") });
|
|
46262
46888
|
} else {
|
|
46263
|
-
const base =
|
|
46889
|
+
const base = join34(root, SKILLS_REL2, slug);
|
|
46264
46890
|
for (const rel of await listFilesRel(base)) {
|
|
46265
|
-
const buf = await readMaybe(
|
|
46891
|
+
const buf = await readMaybe(join34(base, rel));
|
|
46266
46892
|
if (buf) files.push({ path: rel.split("\\").join("/"), contentBase64: buf.toString("base64") });
|
|
46267
46893
|
}
|
|
46268
46894
|
}
|
|
@@ -46280,7 +46906,7 @@ async function artifactSha256(root, kind, slug) {
|
|
|
46280
46906
|
return h.digest("hex");
|
|
46281
46907
|
}
|
|
46282
46908
|
async function artifactExists(root, kind, slug) {
|
|
46283
|
-
const target = kind === "subagent" ?
|
|
46909
|
+
const target = kind === "subagent" ? join34(root, AGENTS_REL2, `${slug}.md`) : join34(root, SKILLS_REL2, slug);
|
|
46284
46910
|
try {
|
|
46285
46911
|
await stat5(target);
|
|
46286
46912
|
return true;
|
|
@@ -46289,7 +46915,7 @@ async function artifactExists(root, kind, slug) {
|
|
|
46289
46915
|
}
|
|
46290
46916
|
}
|
|
46291
46917
|
async function readManifest(root) {
|
|
46292
|
-
const buf = await readMaybe(
|
|
46918
|
+
const buf = await readMaybe(join34(root, MANIFEST_REL));
|
|
46293
46919
|
if (!buf) return {};
|
|
46294
46920
|
try {
|
|
46295
46921
|
return JSON.parse(buf.toString("utf8"));
|
|
@@ -46298,7 +46924,7 @@ async function readManifest(root) {
|
|
|
46298
46924
|
}
|
|
46299
46925
|
}
|
|
46300
46926
|
async function writeManifest(root, manifest) {
|
|
46301
|
-
const path =
|
|
46927
|
+
const path = join34(root, MANIFEST_REL);
|
|
46302
46928
|
await mkdir10(dirname10(path), { recursive: true });
|
|
46303
46929
|
await writeFile13(path, JSON.stringify(manifest, null, 2), "utf8");
|
|
46304
46930
|
}
|
|
@@ -46307,24 +46933,24 @@ async function writeArtifactFiles(root, kind, slug, files) {
|
|
|
46307
46933
|
if (kind === "subagent") {
|
|
46308
46934
|
const agent = byPath.get("agent.md");
|
|
46309
46935
|
if (agent) {
|
|
46310
|
-
await mkdir10(
|
|
46311
|
-
await writeFile13(
|
|
46936
|
+
await mkdir10(join34(root, AGENTS_REL2), { recursive: true });
|
|
46937
|
+
await writeFile13(join34(root, AGENTS_REL2, `${slug}.md`), agent);
|
|
46312
46938
|
}
|
|
46313
46939
|
const short = byPath.get("short-memory.md");
|
|
46314
46940
|
if (short) {
|
|
46315
|
-
await mkdir10(
|
|
46316
|
-
await writeFile13(
|
|
46941
|
+
await mkdir10(join34(root, AGENTS_REL2, "short-memory"), { recursive: true });
|
|
46942
|
+
await writeFile13(join34(root, AGENTS_REL2, "short-memory", `${slug}.md`), short);
|
|
46317
46943
|
}
|
|
46318
46944
|
const long = byPath.get("long-memory.md");
|
|
46319
46945
|
if (long) {
|
|
46320
|
-
await mkdir10(
|
|
46321
|
-
await writeFile13(
|
|
46946
|
+
await mkdir10(join34(root, AGENTS_REL2, "long-memory"), { recursive: true });
|
|
46947
|
+
await writeFile13(join34(root, AGENTS_REL2, "long-memory", `${slug}.md`), long);
|
|
46322
46948
|
}
|
|
46323
46949
|
} else {
|
|
46324
|
-
const base =
|
|
46950
|
+
const base = join34(root, SKILLS_REL2, slug);
|
|
46325
46951
|
for (const [rel, buf] of byPath) {
|
|
46326
46952
|
if (rel.includes("..") || rel.startsWith("/")) continue;
|
|
46327
|
-
const dest =
|
|
46953
|
+
const dest = join34(base, rel);
|
|
46328
46954
|
await mkdir10(dirname10(dest), { recursive: true });
|
|
46329
46955
|
await writeFile13(dest, buf);
|
|
46330
46956
|
}
|
|
@@ -46390,11 +47016,11 @@ async function removePackage(root, slug) {
|
|
|
46390
47016
|
const entry = manifest[n];
|
|
46391
47017
|
if (!entry) return { ok: false, error: "not installed" };
|
|
46392
47018
|
if (entry.kind === "subagent") {
|
|
46393
|
-
await rm8(
|
|
46394
|
-
await rm8(
|
|
46395
|
-
await rm8(
|
|
47019
|
+
await rm8(join34(root, AGENTS_REL2, `${n}.md`), { force: true });
|
|
47020
|
+
await rm8(join34(root, AGENTS_REL2, "short-memory", `${n}.md`), { force: true });
|
|
47021
|
+
await rm8(join34(root, AGENTS_REL2, "long-memory", `${n}.md`), { force: true });
|
|
46396
47022
|
} else {
|
|
46397
|
-
await rm8(
|
|
47023
|
+
await rm8(join34(root, SKILLS_REL2, n), { recursive: true, force: true });
|
|
46398
47024
|
}
|
|
46399
47025
|
delete manifest[n];
|
|
46400
47026
|
await writeManifest(root, manifest);
|
|
@@ -46406,7 +47032,7 @@ async function removePackage(root, slug) {
|
|
|
46406
47032
|
|
|
46407
47033
|
// src/core/graph.ts
|
|
46408
47034
|
import { readFile as readFile18, readdir as readdir8 } from "fs/promises";
|
|
46409
|
-
import { join as
|
|
47035
|
+
import { join as join35, relative as relative2, dirname as dirname11, resolve as resolve12, extname as extname2, basename as basename6 } from "path";
|
|
46410
47036
|
var MAX_FILES = 2e3;
|
|
46411
47037
|
var MAX_NODES = 3e3;
|
|
46412
47038
|
var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", "dist", "build", ".next", "out", "coverage", ".turbo"]);
|
|
@@ -46426,7 +47052,7 @@ async function walk(root, accept) {
|
|
|
46426
47052
|
if (e.name.startsWith(".") && e.name !== ".claude") {
|
|
46427
47053
|
if (e.isDirectory()) continue;
|
|
46428
47054
|
}
|
|
46429
|
-
const full =
|
|
47055
|
+
const full = join35(dir2, e.name);
|
|
46430
47056
|
if (e.isDirectory()) {
|
|
46431
47057
|
if (!SKIP_DIRS.has(e.name)) stack.push(full);
|
|
46432
47058
|
} else if (accept(full)) {
|
|
@@ -46547,15 +47173,15 @@ async function buildGraph(root, mode) {
|
|
|
46547
47173
|
// src/core/rag.ts
|
|
46548
47174
|
import { readFile as readFile19, writeFile as writeFile14, statfs as statfs2, mkdir as mkdir11 } from "fs/promises";
|
|
46549
47175
|
import { existsSync as existsSync17 } from "fs";
|
|
46550
|
-
import { execFile as execFile15, spawn as
|
|
47176
|
+
import { execFile as execFile15, spawn as spawn11 } from "child_process";
|
|
46551
47177
|
import { totalmem as totalmem2, cpus as cpus2 } from "os";
|
|
46552
|
-
import { join as
|
|
47178
|
+
import { join as join36 } from "path";
|
|
46553
47179
|
import { promisify as promisify15 } from "util";
|
|
46554
|
-
var
|
|
47180
|
+
var run15 = promisify15(execFile15);
|
|
46555
47181
|
var RAG_REL = ".claude/rag";
|
|
46556
47182
|
async function tryRun(cmd, args) {
|
|
46557
47183
|
try {
|
|
46558
|
-
const { stdout } = await
|
|
47184
|
+
const { stdout } = await run15(cmd, args, { timeout: 8e3 });
|
|
46559
47185
|
return { ok: true, out: stdout.trim() };
|
|
46560
47186
|
} catch {
|
|
46561
47187
|
return { ok: false, out: "" };
|
|
@@ -46570,7 +47196,7 @@ async function detectPython() {
|
|
|
46570
47196
|
}
|
|
46571
47197
|
async function installLog(root) {
|
|
46572
47198
|
try {
|
|
46573
|
-
const text = await readFile19(
|
|
47199
|
+
const text = await readFile19(join36(root, RAG_REL, "install.log"), "utf8");
|
|
46574
47200
|
return text.split("\n").filter((l) => l.length > 0).slice(-200);
|
|
46575
47201
|
} catch {
|
|
46576
47202
|
return [];
|
|
@@ -46632,14 +47258,14 @@ async function ragStatus(root) {
|
|
|
46632
47258
|
};
|
|
46633
47259
|
let modelName = "";
|
|
46634
47260
|
try {
|
|
46635
|
-
modelName = (await readFile19(
|
|
47261
|
+
modelName = (await readFile19(join36(root, RAG_REL, ".installed"), "utf8")).trim();
|
|
46636
47262
|
} catch {
|
|
46637
47263
|
modelName = "";
|
|
46638
47264
|
}
|
|
46639
47265
|
const modelPresent = modelName.length > 0;
|
|
46640
47266
|
return {
|
|
46641
47267
|
installed: fastembed && sqliteVec && modelPresent,
|
|
46642
|
-
installing: existsSync17(
|
|
47268
|
+
installing: existsSync17(join36(root, RAG_REL, ".installing")),
|
|
46643
47269
|
python: { found: py.found, version: py.version },
|
|
46644
47270
|
deps: { pip, fastembed, sqliteVec },
|
|
46645
47271
|
model: { present: modelPresent, name: modelName },
|
|
@@ -46650,9 +47276,9 @@ async function ragStatus(root) {
|
|
|
46650
47276
|
};
|
|
46651
47277
|
}
|
|
46652
47278
|
async function readIndexMeta(root) {
|
|
46653
|
-
const indexing = existsSync17(
|
|
47279
|
+
const indexing = existsSync17(join36(root, RAG_REL, ".indexing"));
|
|
46654
47280
|
try {
|
|
46655
|
-
const m2 = JSON.parse(await readFile19(
|
|
47281
|
+
const m2 = JSON.parse(await readFile19(join36(root, RAG_REL, ".index.json"), "utf8"));
|
|
46656
47282
|
return {
|
|
46657
47283
|
present: true,
|
|
46658
47284
|
indexing,
|
|
@@ -46719,14 +47345,14 @@ if __name__ == "__main__":
|
|
|
46719
47345
|
elif cmd == "query": query(sys.argv[2], int(sys.argv[3]) if len(sys.argv) > 3 else 8)
|
|
46720
47346
|
`;
|
|
46721
47347
|
async function ensureEngine(root) {
|
|
46722
|
-
await mkdir11(
|
|
46723
|
-
await writeFile14(
|
|
47348
|
+
await mkdir11(join36(root, RAG_REL), { recursive: true }).catch(() => void 0);
|
|
47349
|
+
await writeFile14(join36(root, RAG_REL, "rag_engine.py"), RAG_ENGINE_PY, "utf8");
|
|
46724
47350
|
}
|
|
46725
47351
|
async function ragReindex(root) {
|
|
46726
47352
|
const py = await detectPython();
|
|
46727
47353
|
if (!py.found) return { ok: false, started: false, error: "python3 not found" };
|
|
46728
|
-
if (!existsSync17(
|
|
46729
|
-
if (existsSync17(
|
|
47354
|
+
if (!existsSync17(join36(root, RAG_REL, ".installed"))) return { ok: false, started: false, error: "RAG is not installed" };
|
|
47355
|
+
if (existsSync17(join36(root, RAG_REL, ".indexing"))) return { ok: true, started: true };
|
|
46730
47356
|
await ensureEngine(root);
|
|
46731
47357
|
const script = `
|
|
46732
47358
|
set +e
|
|
@@ -46734,7 +47360,7 @@ touch "${RAG_REL}/.indexing"
|
|
|
46734
47360
|
{ echo "[reindex] $(date)"; "$RAG_PY" "${RAG_REL}/rag_engine.py" reindex 2>&1; } >> "${RAG_REL}/index.log" 2>&1
|
|
46735
47361
|
rm -f "${RAG_REL}/.indexing"
|
|
46736
47362
|
`;
|
|
46737
|
-
const child =
|
|
47363
|
+
const child = spawn11("bash", ["-c", script], {
|
|
46738
47364
|
cwd: root,
|
|
46739
47365
|
env: { ...process.env, RAG_PY: py.bin },
|
|
46740
47366
|
detached: true,
|
|
@@ -46746,12 +47372,12 @@ rm -f "${RAG_REL}/.indexing"
|
|
|
46746
47372
|
async function ragQuery(root, queryText, k2 = 8) {
|
|
46747
47373
|
const py = await detectPython();
|
|
46748
47374
|
if (!py.found) return { results: [], error: "python3 not found" };
|
|
46749
|
-
if (!existsSync17(
|
|
47375
|
+
if (!existsSync17(join36(root, RAG_REL, "index.db"))) return { results: [], error: "no index \u2014 reindex first" };
|
|
46750
47376
|
await ensureEngine(root);
|
|
46751
47377
|
try {
|
|
46752
|
-
const { stdout } = await
|
|
47378
|
+
const { stdout } = await run15(
|
|
46753
47379
|
py.bin,
|
|
46754
|
-
[
|
|
47380
|
+
[join36(RAG_REL, "rag_engine.py"), "query", queryText, String(Math.min(Math.max(k2, 1), 20))],
|
|
46755
47381
|
{ cwd: root, timeout: 3e4, maxBuffer: 4 * 1024 * 1024 }
|
|
46756
47382
|
);
|
|
46757
47383
|
const parsed = JSON.parse(stdout.trim() || '{"results":[]}');
|
|
@@ -46766,9 +47392,9 @@ async function ragInstall(root, model) {
|
|
|
46766
47392
|
}
|
|
46767
47393
|
const py = await detectPython();
|
|
46768
47394
|
if (!py.found) return { ok: false, started: false, error: "python3 not found on the worker" };
|
|
46769
|
-
if (existsSync17(
|
|
47395
|
+
if (existsSync17(join36(root, RAG_REL, ".installing"))) return { ok: true, started: true };
|
|
46770
47396
|
try {
|
|
46771
|
-
await mkdir11(
|
|
47397
|
+
await mkdir11(join36(root, RAG_REL), { recursive: true });
|
|
46772
47398
|
} catch {
|
|
46773
47399
|
}
|
|
46774
47400
|
const script = `
|
|
@@ -46784,7 +47410,7 @@ rm -f "${RAG_REL}/.installed"
|
|
|
46784
47410
|
} >> "${RAG_REL}/install.log" 2>&1
|
|
46785
47411
|
rm -f "${RAG_REL}/.installing"
|
|
46786
47412
|
`;
|
|
46787
|
-
const child =
|
|
47413
|
+
const child = spawn11("bash", ["-c", script], {
|
|
46788
47414
|
cwd: root,
|
|
46789
47415
|
env: { ...process.env, RAG_PY: py.bin, RAG_MODEL: model },
|
|
46790
47416
|
detached: true,
|
|
@@ -46796,7 +47422,7 @@ rm -f "${RAG_REL}/.installing"
|
|
|
46796
47422
|
|
|
46797
47423
|
// src/core/config-sync.ts
|
|
46798
47424
|
import { existsSync as existsSync18, readFileSync as readFileSync16 } from "fs";
|
|
46799
|
-
import { join as
|
|
47425
|
+
import { join as join37 } from "path";
|
|
46800
47426
|
var SERVER_MANAGED_KEYS = [
|
|
46801
47427
|
"physicPath",
|
|
46802
47428
|
"sessionSwitchPct",
|
|
@@ -46813,7 +47439,7 @@ var SERVER_MANAGED_KEYS = [
|
|
|
46813
47439
|
"projectAiMemoryBudgetChars"
|
|
46814
47440
|
];
|
|
46815
47441
|
function readConfigText(profileDir2) {
|
|
46816
|
-
const path =
|
|
47442
|
+
const path = join37(profileDir2, "config.json");
|
|
46817
47443
|
if (existsSync18(path)) {
|
|
46818
47444
|
try {
|
|
46819
47445
|
return readFileSync16(path, "utf8");
|
|
@@ -47439,6 +48065,7 @@ var WsClient = class {
|
|
|
47439
48065
|
if (token.aiMemory && !this.aiMemory) this.aiMemory = token.aiMemory.text;
|
|
47440
48066
|
this.outboundReviewEnabled = token.outboundReview?.enabled === true;
|
|
47441
48067
|
this.restrictToFolder = token.aiRestrictToFolder === true;
|
|
48068
|
+
setPromptOverrides(token.promptOverrides ?? null);
|
|
47442
48069
|
configureGitAuth(
|
|
47443
48070
|
token.gitAuth ?? null,
|
|
47444
48071
|
token.gitAuthHost ?? null,
|
|
@@ -47604,6 +48231,7 @@ var WsClient = class {
|
|
|
47604
48231
|
if (handleToolsChannels(this.hctx, message, payload)) return;
|
|
47605
48232
|
if (handleGitChannels(this.hctx, message, payload)) return;
|
|
47606
48233
|
if (handleSupportChannels(this.hctx, message, payload)) return;
|
|
48234
|
+
if (handleResearchChannels(this.hctx, message, payload)) return;
|
|
47607
48235
|
if (handleMiscChannels(this.hctx, message, payload)) return;
|
|
47608
48236
|
this.bus.log(t("error.unsupportedChannel", { channel: message.channel }), "warn");
|
|
47609
48237
|
}
|
|
@@ -47722,7 +48350,7 @@ var WsClient = class {
|
|
|
47722
48350
|
*/
|
|
47723
48351
|
physicFolderPath(projectName) {
|
|
47724
48352
|
const safe = projectName.replace(/[/\\]/g, "_").replace(/\.\./g, "_").trim();
|
|
47725
|
-
return safe ?
|
|
48353
|
+
return safe ? join38(this.context.profileDir, safe) : null;
|
|
47726
48354
|
}
|
|
47727
48355
|
/**
|
|
47728
48356
|
* Report machine.status: version + the state of the physic project being served
|
|
@@ -47834,7 +48462,7 @@ var WsClient = class {
|
|
|
47834
48462
|
*/
|
|
47835
48463
|
uploadLog() {
|
|
47836
48464
|
try {
|
|
47837
|
-
const upload = readLogUpload(
|
|
48465
|
+
const upload = readLogUpload(join38(this.context.profileDir, "logs"));
|
|
47838
48466
|
if (!upload) return;
|
|
47839
48467
|
const sig = `${upload.fileName}:${upload.totalBytes}`;
|
|
47840
48468
|
if (sig === this.lastLogSig) return;
|
|
@@ -47996,10 +48624,10 @@ var WsClient = class {
|
|
|
47996
48624
|
};
|
|
47997
48625
|
|
|
47998
48626
|
// src/core/instance-lock.ts
|
|
47999
|
-
import { readFileSync as readFileSync17, rmSync as
|
|
48000
|
-
import { join as
|
|
48627
|
+
import { readFileSync as readFileSync17, rmSync as rmSync10, writeFileSync as writeFileSync10 } from "fs";
|
|
48628
|
+
import { join as join39 } from "path";
|
|
48001
48629
|
function lockPath(profileDir2) {
|
|
48002
|
-
return
|
|
48630
|
+
return join39(profileDir2, "start.lock");
|
|
48003
48631
|
}
|
|
48004
48632
|
function isAlive(pid) {
|
|
48005
48633
|
try {
|
|
@@ -48019,13 +48647,13 @@ function lockHolder(profileDir2) {
|
|
|
48019
48647
|
}
|
|
48020
48648
|
function acquireInstanceLock(profileDir2) {
|
|
48021
48649
|
if (lockHolder(profileDir2) !== null) return false;
|
|
48022
|
-
|
|
48650
|
+
writeFileSync10(lockPath(profileDir2), String(process.pid), "utf8");
|
|
48023
48651
|
return true;
|
|
48024
48652
|
}
|
|
48025
48653
|
function releaseInstanceLock(profileDir2) {
|
|
48026
48654
|
try {
|
|
48027
48655
|
const pid = Number.parseInt(readFileSync17(lockPath(profileDir2), "utf8").trim(), 10);
|
|
48028
|
-
if (pid === process.pid)
|
|
48656
|
+
if (pid === process.pid) rmSync10(lockPath(profileDir2), { force: true });
|
|
48029
48657
|
} catch {
|
|
48030
48658
|
}
|
|
48031
48659
|
}
|
|
@@ -48320,8 +48948,8 @@ var SessionBus = class {
|
|
|
48320
48948
|
|
|
48321
48949
|
// src/core/control-server.ts
|
|
48322
48950
|
import { createServer } from "net";
|
|
48323
|
-
import { chmodSync as chmodSync5, rmSync as
|
|
48324
|
-
import { join as
|
|
48951
|
+
import { chmodSync as chmodSync5, rmSync as rmSync11 } from "fs";
|
|
48952
|
+
import { join as join41 } from "path";
|
|
48325
48953
|
|
|
48326
48954
|
// src/core/control-protocol.ts
|
|
48327
48955
|
var CONTROL_SOCKET_FILE = "control.sock";
|
|
@@ -48350,16 +48978,16 @@ function createFrameParser() {
|
|
|
48350
48978
|
}
|
|
48351
48979
|
|
|
48352
48980
|
// src/core/control-token.ts
|
|
48353
|
-
import { chmodSync as chmodSync4, readFileSync as readFileSync18, writeFileSync as
|
|
48981
|
+
import { chmodSync as chmodSync4, readFileSync as readFileSync18, writeFileSync as writeFileSync11 } from "fs";
|
|
48354
48982
|
import { randomBytes as randomBytes3, timingSafeEqual } from "crypto";
|
|
48355
|
-
import { join as
|
|
48983
|
+
import { join as join40 } from "path";
|
|
48356
48984
|
function controlTokenPath(profileDir2) {
|
|
48357
|
-
return
|
|
48985
|
+
return join40(profileDir2, CONTROL_TOKEN_FILE);
|
|
48358
48986
|
}
|
|
48359
48987
|
function writeControlToken(profileDir2) {
|
|
48360
48988
|
const token = randomBytes3(32).toString("hex");
|
|
48361
48989
|
const path = controlTokenPath(profileDir2);
|
|
48362
|
-
|
|
48990
|
+
writeFileSync11(path, token, { mode: 384 });
|
|
48363
48991
|
try {
|
|
48364
48992
|
chmodSync4(path, 384);
|
|
48365
48993
|
} catch {
|
|
@@ -48383,9 +49011,9 @@ function tokenMatches(expected, given) {
|
|
|
48383
49011
|
|
|
48384
49012
|
// src/core/control-server.ts
|
|
48385
49013
|
function startControlServer(bus, profileDir2, info) {
|
|
48386
|
-
const socketPath =
|
|
49014
|
+
const socketPath = join41(profileDir2, CONTROL_SOCKET_FILE);
|
|
48387
49015
|
try {
|
|
48388
|
-
|
|
49016
|
+
rmSync11(socketPath, { force: true });
|
|
48389
49017
|
} catch {
|
|
48390
49018
|
}
|
|
48391
49019
|
const clients = /* @__PURE__ */ new Set();
|
|
@@ -48476,8 +49104,8 @@ function startControlServer(bus, profileDir2, info) {
|
|
|
48476
49104
|
clients.clear();
|
|
48477
49105
|
server.close();
|
|
48478
49106
|
try {
|
|
48479
|
-
|
|
48480
|
-
|
|
49107
|
+
rmSync11(socketPath, { force: true });
|
|
49108
|
+
rmSync11(controlTokenPath(profileDir2), { force: true });
|
|
48481
49109
|
} catch {
|
|
48482
49110
|
}
|
|
48483
49111
|
};
|
|
@@ -50456,7 +51084,7 @@ var src_default = Yoga;
|
|
|
50456
51084
|
|
|
50457
51085
|
// ../../node_modules/.pnpm/ink@5.2.1_@types+react@18.3.31_react@18.3.1/node_modules/ink/build/reconciler.js
|
|
50458
51086
|
var import_react_reconciler = __toESM(require_react_reconciler(), 1);
|
|
50459
|
-
var
|
|
51087
|
+
var import_constants15 = __toESM(require_constants2(), 1);
|
|
50460
51088
|
import process3 from "process";
|
|
50461
51089
|
|
|
50462
51090
|
// ../../node_modules/.pnpm/ansi-regex@6.2.2/node_modules/ansi-regex/index.js
|
|
@@ -51698,7 +52326,7 @@ var reconciler_default = (0, import_react_reconciler.default)({
|
|
|
51698
52326
|
scheduleTimeout: setTimeout,
|
|
51699
52327
|
cancelTimeout: clearTimeout,
|
|
51700
52328
|
noTimeout: -1,
|
|
51701
|
-
getCurrentEventPriority: () =>
|
|
52329
|
+
getCurrentEventPriority: () => import_constants15.DefaultEventPriority,
|
|
51702
52330
|
beforeActiveInstanceBlur() {
|
|
51703
52331
|
},
|
|
51704
52332
|
afterActiveInstanceBlur() {
|
|
@@ -54140,11 +54768,11 @@ var import_react21 = __toESM(require_react(), 1);
|
|
|
54140
54768
|
var import_react26 = __toESM(require_react(), 1);
|
|
54141
54769
|
|
|
54142
54770
|
// src/core/input-history.ts
|
|
54143
|
-
import { existsSync as existsSync21, readFileSync as readFileSync20, writeFileSync as
|
|
54144
|
-
import { join as
|
|
54771
|
+
import { existsSync as existsSync21, readFileSync as readFileSync20, writeFileSync as writeFileSync12 } from "fs";
|
|
54772
|
+
import { join as join42 } from "path";
|
|
54145
54773
|
var MAX_HISTORY = 200;
|
|
54146
54774
|
function historyPath(profileDir2) {
|
|
54147
|
-
return
|
|
54775
|
+
return join42(profileDir2, "input-history.json");
|
|
54148
54776
|
}
|
|
54149
54777
|
function loadInputHistory(profileDir2) {
|
|
54150
54778
|
const path = historyPath(profileDir2);
|
|
@@ -54161,7 +54789,7 @@ function appendInputHistory(profileDir2, line, current2) {
|
|
|
54161
54789
|
if (!trimmed || current2[current2.length - 1] === trimmed) return current2;
|
|
54162
54790
|
const next = [...current2, trimmed].slice(-MAX_HISTORY);
|
|
54163
54791
|
try {
|
|
54164
|
-
|
|
54792
|
+
writeFileSync12(historyPath(profileDir2), JSON.stringify(next), "utf8");
|
|
54165
54793
|
} catch {
|
|
54166
54794
|
}
|
|
54167
54795
|
return next;
|
|
@@ -54758,7 +55386,7 @@ async function runStart(profileDir2, profileName) {
|
|
|
54758
55386
|
return;
|
|
54759
55387
|
}
|
|
54760
55388
|
logger.configure({
|
|
54761
|
-
dir:
|
|
55389
|
+
dir: join43(profileDir2, "logs"),
|
|
54762
55390
|
level: resolveLogLevel(),
|
|
54763
55391
|
base: { version: CLI_VERSION, profile: profileName, scope: credential.scope }
|
|
54764
55392
|
});
|
|
@@ -54778,7 +55406,7 @@ async function runStart(profileDir2, profileName) {
|
|
|
54778
55406
|
if (result.action === "updated") {
|
|
54779
55407
|
logger.info("update.updated", { version: result.version });
|
|
54780
55408
|
console.log(t("start.restarting"));
|
|
54781
|
-
const child =
|
|
55409
|
+
const child = spawn12(process.execPath, process.argv.slice(1), {
|
|
54782
55410
|
stdio: "inherit",
|
|
54783
55411
|
env: { ...process.env, FOURPM_NO_UPDATE: "1" }
|
|
54784
55412
|
});
|
|
@@ -54868,7 +55496,7 @@ async function runStart(profileDir2, profileName) {
|
|
|
54868
55496
|
}
|
|
54869
55497
|
|
|
54870
55498
|
// src/commands/attach.ts
|
|
54871
|
-
import { join as
|
|
55499
|
+
import { join as join44 } from "path";
|
|
54872
55500
|
|
|
54873
55501
|
// src/core/control-client.ts
|
|
54874
55502
|
import { connect as connect2 } from "net";
|
|
@@ -54977,7 +55605,7 @@ async function runAttach(profileDir2, profileName) {
|
|
|
54977
55605
|
process.exitCode = 1;
|
|
54978
55606
|
return;
|
|
54979
55607
|
}
|
|
54980
|
-
const socketPath =
|
|
55608
|
+
const socketPath = join44(profileDir2, CONTROL_SOCKET_FILE);
|
|
54981
55609
|
const bus = new SessionBus();
|
|
54982
55610
|
let conn;
|
|
54983
55611
|
try {
|
|
@@ -55008,11 +55636,11 @@ async function runAttach(profileDir2, profileName) {
|
|
|
55008
55636
|
|
|
55009
55637
|
// src/commands/auto-run.ts
|
|
55010
55638
|
import { connect as connect3 } from "net";
|
|
55011
|
-
import { join as
|
|
55639
|
+
import { join as join45 } from "path";
|
|
55012
55640
|
var AUTO_RUN_TIMEOUT_MS = 3 * 60 * 60 * 1e3;
|
|
55013
55641
|
async function runAutoRun(profileDir2, profileName) {
|
|
55014
55642
|
initI18n(readProfileConfig(profileDir2).locale);
|
|
55015
|
-
const socketPath =
|
|
55643
|
+
const socketPath = join45(profileDir2, CONTROL_SOCKET_FILE);
|
|
55016
55644
|
const token = readControlToken(profileDir2);
|
|
55017
55645
|
const code = await new Promise((resolve13) => {
|
|
55018
55646
|
let settled = false;
|
|
@@ -55069,7 +55697,7 @@ async function runAutoRun(profileDir2, profileName) {
|
|
|
55069
55697
|
|
|
55070
55698
|
// src/commands/git-credential.ts
|
|
55071
55699
|
import { connect as connect4 } from "net";
|
|
55072
|
-
import { join as
|
|
55700
|
+
import { join as join46 } from "path";
|
|
55073
55701
|
var REQUEST_TIMEOUT_MS = 2e4;
|
|
55074
55702
|
function requestToken(host, path) {
|
|
55075
55703
|
const profileDir2 = process.env[PROFILE_DIR_ENV];
|
|
@@ -55084,7 +55712,7 @@ function requestToken(host, path) {
|
|
|
55084
55712
|
socket.destroy();
|
|
55085
55713
|
resolve13(value);
|
|
55086
55714
|
};
|
|
55087
|
-
const socket = connect4(
|
|
55715
|
+
const socket = connect4(join46(profileDir2, CONTROL_SOCKET_FILE));
|
|
55088
55716
|
socket.setEncoding("utf8");
|
|
55089
55717
|
const timer = setTimeout(() => finish(null), REQUEST_TIMEOUT_MS);
|
|
55090
55718
|
socket.on("connect", () => {
|
|
@@ -55125,7 +55753,8 @@ async function runGitCredential(action) {
|
|
|
55125
55753
|
const host = (input.host ?? "").toLowerCase();
|
|
55126
55754
|
if (input.protocol !== "https" || !host || host !== (process.env[GIT_HOST_ENV] ?? "").toLowerCase()) return;
|
|
55127
55755
|
const token = await requestToken(host, input.path ?? "");
|
|
55128
|
-
|
|
55756
|
+
const username = (process.env[GIT_HOST_KIND_ENV] ?? "") === "gitlab" ? "oauth2" : "x-access-token";
|
|
55757
|
+
if (token) process.stdout.write(`username=${username}
|
|
55129
55758
|
password=${token}
|
|
55130
55759
|
`);
|
|
55131
55760
|
}
|
|
@@ -55138,12 +55767,12 @@ async function runGitToken() {
|
|
|
55138
55767
|
}
|
|
55139
55768
|
|
|
55140
55769
|
// src/commands/ai-login.ts
|
|
55141
|
-
import { spawn as
|
|
55770
|
+
import { spawn as spawn13 } from "child_process";
|
|
55142
55771
|
import { mkdirSync as mkdirSync10 } from "fs";
|
|
55143
55772
|
import { basename as basename8 } from "path";
|
|
55144
55773
|
function runProviderLogin(cmd, envVar, dir2) {
|
|
55145
55774
|
return new Promise((resolve13) => {
|
|
55146
|
-
const child =
|
|
55775
|
+
const child = spawn13(cmd, [], {
|
|
55147
55776
|
stdio: "inherit",
|
|
55148
55777
|
env: { ...process.env, [envVar]: dir2 }
|
|
55149
55778
|
});
|
|
@@ -55188,7 +55817,7 @@ async function runAiLogin(profileDir2, profileName, aiFilter) {
|
|
|
55188
55817
|
}
|
|
55189
55818
|
|
|
55190
55819
|
// src/commands/unlink.ts
|
|
55191
|
-
import { rmSync as
|
|
55820
|
+
import { rmSync as rmSync12 } from "fs";
|
|
55192
55821
|
async function runUnlink(profileDir2, profileName) {
|
|
55193
55822
|
initI18n(readProfileConfig(profileDir2).locale);
|
|
55194
55823
|
const cred = readCredential(profileDir2, profileName);
|
|
@@ -55202,7 +55831,7 @@ async function runUnlink(profileDir2, profileName) {
|
|
|
55202
55831
|
} catch (err) {
|
|
55203
55832
|
console.log(t("unlink.revokeFailed", { message: err.message }));
|
|
55204
55833
|
}
|
|
55205
|
-
|
|
55834
|
+
rmSync12(profileDir2, { recursive: true, force: true });
|
|
55206
55835
|
clearDefaultProfileIf(profileName);
|
|
55207
55836
|
console.log(t("unlink.deleted"));
|
|
55208
55837
|
}
|