guardian-framework 0.1.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/LICENSE +21 -0
- package/README.md +463 -0
- package/dist/cli.js +12958 -0
- package/dist/exports.js +12765 -0
- package/package.json +61 -0
- package/templates/languages/go-patterns.md +411 -0
- package/templates/languages/java-patterns.md +545 -0
- package/templates/languages/python-patterns.md +312 -0
- package/templates/languages/rust-patterns.md +232 -0
- package/templates/languages/typescript-patterns.md +313 -0
- package/templates/pi/INDEX.md +437 -0
- package/templates/pi/agent/AGENTS.md +166 -0
- package/templates/pi/agents/architecture-coordinator.md +40 -0
- package/templates/pi/agents/architecture-validator.md +36 -0
- package/templates/pi/agents/bootstrap-implementer.md +37 -0
- package/templates/pi/agents/issue-factory.md +31 -0
- package/templates/pi/agents/operations-validator.md +32 -0
- package/templates/pi/agents/security-validator.md +33 -0
- package/templates/pi/architecture/CHANGELOG.md +78 -0
- package/templates/pi/architecture/decisions/ADR-template.md +76 -0
- package/templates/pi/architecture/diagrams/system-overview.md +159 -0
- package/templates/pi/architecture/modules/module-template.md +152 -0
- package/templates/pi/context/checklists.md +91 -0
- package/templates/pi/context/domain-workflow.md +180 -0
- package/templates/pi/context/output-formats.md +135 -0
- package/templates/pi/context/patterns-base.md +47 -0
- package/templates/pi/context/patterns.md +38 -0
- package/templates/pi/context/project.md +88 -0
- package/templates/pi/domain/exploration.md +93 -0
- package/templates/pi/domain/ubiquitous-language.md +24 -0
- package/templates/pi/extensions/architect-lib/generators.ts +528 -0
- package/templates/pi/extensions/architect-lib/helpers.ts +344 -0
- package/templates/pi/extensions/architect-lib/types.ts +117 -0
- package/templates/pi/extensions/architect.ts +1425 -0
- package/templates/pi/extensions/ask-user-question.ts +579 -0
- package/templates/pi/extensions/bash-guard.ts +556 -0
- package/templates/pi/extensions/config-reload.ts +115 -0
- package/templates/pi/extensions/coordinator.ts +304 -0
- package/templates/pi/extensions/curator.ts +543 -0
- package/templates/pi/extensions/domain-explorer.ts +990 -0
- package/templates/pi/extensions/filechanges.ts +656 -0
- package/templates/pi/extensions/goal-loop.ts +717 -0
- package/templates/pi/extensions/hooks.ts +452 -0
- package/templates/pi/extensions/kanban.ts +547 -0
- package/templates/pi/extensions/pipeline.ts +1246 -0
- package/templates/pi/extensions/plan-mode.ts +209 -0
- package/templates/pi/extensions/project-scaffolder.ts +284 -0
- package/templates/pi/extensions/read-only-mode.ts +192 -0
- package/templates/pi/extensions/redaction.ts +128 -0
- package/templates/pi/extensions/session-persistence.ts +174 -0
- package/templates/pi/extensions/slash-commands.ts +229 -0
- package/templates/pi/extensions/snippets.ts +201 -0
- package/templates/pi/extensions/validation-runner.ts +100 -0
- package/templates/pi/github/agents/architecture-coordinator.agent.md +27 -0
- package/templates/pi/github/agents/architecture-validator.agent.md +20 -0
- package/templates/pi/github/agents/bootstrap-implementer.agent.md +20 -0
- package/templates/pi/github/agents/epic-planner.agent.md +70 -0
- package/templates/pi/github/agents/issue-factory.agent.md +20 -0
- package/templates/pi/github/agents/operations-validator.agent.md +20 -0
- package/templates/pi/github/agents/security-validator.agent.md +20 -0
- package/templates/pi/github/copilot/settings.json +16 -0
- package/templates/pi/github/copilot-instructions.md +155 -0
- package/templates/pi/github/instructions/architecture.instructions.md +71 -0
- package/templates/pi/github/instructions/validation.instructions.md +69 -0
- package/templates/pi/github/workflows/01-planning-workflow.md +75 -0
- package/templates/pi/github/workflows/02-issue-generation-workflow.md +69 -0
- package/templates/pi/github/workflows/03-implementation-workflow.md +80 -0
- package/templates/pi/github/workflows/04-validation-workflow.md +68 -0
- package/templates/pi/preflight_report.json +99 -0
- package/templates/pi/prompts/blueprint-update.md +262 -0
- package/templates/pi/prompts/blueprint-validate.md +218 -0
- package/templates/pi/prompts/bug-fix.md +50 -0
- package/templates/pi/prompts/ci-blueprint.md +131 -0
- package/templates/pi/prompts/context-refresh.md +258 -0
- package/templates/pi/prompts/epic-plan.md +356 -0
- package/templates/pi/prompts/epic-template.md +186 -0
- package/templates/pi/prompts/feature-development.md +131 -0
- package/templates/pi/prompts/git-issues.md +243 -0
- package/templates/pi/prompts/hotfix.md +71 -0
- package/templates/pi/prompts/issue-closeout.md +342 -0
- package/templates/pi/prompts/issue-draft.md +218 -0
- package/templates/pi/prompts/issue-implementation-series.md +173 -0
- package/templates/pi/prompts/issue-merge.md +372 -0
- package/templates/pi/prompts/issue-template-set.md +392 -0
- package/templates/pi/prompts/issue-template.md +207 -0
- package/templates/pi/prompts/pattern-extract.md +269 -0
- package/templates/pi/prompts/plan-to-issues.md +430 -0
- package/templates/pi/prompts/refactoring.md +82 -0
- package/templates/pi/prompts/scope-analyzer.md +254 -0
- package/templates/pi/prompts/sync-check.md +230 -0
- package/templates/pi/scripts/categorize-issues.sh +171 -0
- package/templates/pi/scripts/ci/check_architecture_conformance.sh +748 -0
- package/templates/pi/scripts/ci/check_planning_packet.py +228 -0
- package/templates/pi/scripts/ci/run_hardening_stages.sh +264 -0
- package/templates/pi/scripts/ci/run_preflight.sh +649 -0
- package/templates/pi/scripts/ci/run_stage.sh +193 -0
- package/templates/pi/scripts/ci/stage_docs_policy.sh +82 -0
- package/templates/pi/scripts/ci/stage_integration.sh +3 -0
- package/templates/pi/scripts/ci/stage_lint.sh +130 -0
- package/templates/pi/scripts/ci/stage_migration_verify.sh +3 -0
- package/templates/pi/scripts/ci/stage_package_build.sh +3 -0
- package/templates/pi/scripts/ci/stage_release_readiness.sh +3 -0
- package/templates/pi/scripts/ci/stage_remaining.sh +242 -0
- package/templates/pi/scripts/ci/stage_security.sh +3 -0
- package/templates/pi/scripts/ci/stage_static_analysis.sh +114 -0
- package/templates/pi/scripts/ci/stage_unit.sh +119 -0
- package/templates/pi/scripts/ci/validate_agent_output.py +181 -0
- package/templates/pi/scripts/ci/validate_agent_output.sh +243 -0
- package/templates/pi/scripts/create-feature-branch.sh +136 -0
- package/templates/pi/scripts/create-mr.sh +117 -0
- package/templates/pi/scripts/fetch-issues.sh +131 -0
- package/templates/pi/scripts/generate-architecture.sh +161 -0
- package/templates/pi/scripts/git/close-epic.sh +69 -0
- package/templates/pi/scripts/git/close-issue.sh +35 -0
- package/templates/pi/scripts/git/create-tracking-issue.sh +174 -0
- package/templates/pi/scripts/git/link-issue-to-epic.sh +51 -0
- package/templates/pi/scripts/git/update-tracking-issue.sh +66 -0
- package/templates/pi/scripts/languages/go/validate-architecture.sh +139 -0
- package/templates/pi/scripts/languages/go/validate-canonical.sh +129 -0
- package/templates/pi/scripts/languages/go/validate-ci.sh +132 -0
- package/templates/pi/scripts/languages/go/validate-integration.sh +156 -0
- package/templates/pi/scripts/languages/go/validate-operations.sh +146 -0
- package/templates/pi/scripts/languages/go/validate-security.sh +135 -0
- package/templates/pi/scripts/languages/go/validate-tests.sh +125 -0
- package/templates/pi/scripts/languages/java/validate-annotations.sh +185 -0
- package/templates/pi/scripts/languages/java/validate-architecture.sh +159 -0
- package/templates/pi/scripts/languages/java/validate-canonical.sh +114 -0
- package/templates/pi/scripts/languages/java/validate-ci.sh +269 -0
- package/templates/pi/scripts/languages/java/validate-integration.sh +149 -0
- package/templates/pi/scripts/languages/java/validate-operations.sh +142 -0
- package/templates/pi/scripts/languages/java/validate-security.sh +174 -0
- package/templates/pi/scripts/languages/java/validate-spring-architecture.sh +227 -0
- package/templates/pi/scripts/languages/java/validate-tests.sh +159 -0
- package/templates/pi/scripts/languages/python/validate-architecture.sh +99 -0
- package/templates/pi/scripts/languages/python/validate-canonical.sh +124 -0
- package/templates/pi/scripts/languages/python/validate-ci.sh +130 -0
- package/templates/pi/scripts/languages/python/validate-integration.sh +119 -0
- package/templates/pi/scripts/languages/python/validate-operations.sh +107 -0
- package/templates/pi/scripts/languages/python/validate-security.sh +68 -0
- package/templates/pi/scripts/languages/python/validate-tests.sh +102 -0
- package/templates/pi/scripts/languages/rust/validate-architecture.sh +188 -0
- package/templates/pi/scripts/languages/rust/validate-canonical.sh +146 -0
- package/templates/pi/scripts/languages/rust/validate-ci.sh +147 -0
- package/templates/pi/scripts/languages/rust/validate-integration.sh +149 -0
- package/templates/pi/scripts/languages/rust/validate-operations.sh +154 -0
- package/templates/pi/scripts/languages/rust/validate-security.sh +153 -0
- package/templates/pi/scripts/languages/rust/validate-tests.sh +150 -0
- package/templates/pi/scripts/languages/typescript/validate-architecture.sh +157 -0
- package/templates/pi/scripts/languages/typescript/validate-canonical.sh +135 -0
- package/templates/pi/scripts/languages/typescript/validate-ci.sh +244 -0
- package/templates/pi/scripts/languages/typescript/validate-integration.sh +166 -0
- package/templates/pi/scripts/languages/typescript/validate-operations.sh +162 -0
- package/templates/pi/scripts/languages/typescript/validate-security.sh +169 -0
- package/templates/pi/scripts/languages/typescript/validate-tests.sh +158 -0
- package/templates/pi/scripts/merge-mr.sh +95 -0
- package/templates/pi/scripts/mr-validation.sh +228 -0
- package/templates/pi/scripts/validate-architecture-readiness.sh +153 -0
- package/templates/pi/scripts/validate-architecture.sh +22 -0
- package/templates/pi/scripts/validate-canonical.sh +22 -0
- package/templates/pi/scripts/validate-ci.sh +37 -0
- package/templates/pi/scripts/validate-integration.sh +22 -0
- package/templates/pi/scripts/validate-operations.sh +22 -0
- package/templates/pi/scripts/validate-security.sh +22 -0
- package/templates/pi/scripts/validate-tests.sh +22 -0
- package/templates/pi/scripts/validate-ubiquitous-language.sh +291 -0
- package/templates/pi/scripts/validation-cache.sh +139 -0
- package/templates/pi/skills/agents/architecture-coordinator.md +178 -0
- package/templates/pi/skills/agents/architecture-generator.md +79 -0
- package/templates/pi/skills/agents/architecture-validator.md +74 -0
- package/templates/pi/skills/agents/ci-mr-validator.md +46 -0
- package/templates/pi/skills/agents/code-developer.md +76 -0
- package/templates/pi/skills/agents/commit.md +46 -0
- package/templates/pi/skills/agents/curator.md +71 -0
- package/templates/pi/skills/agents/debug.md +59 -0
- package/templates/pi/skills/agents/documentation-maintainer.md +36 -0
- package/templates/pi/skills/agents/goal-loop.md +125 -0
- package/templates/pi/skills/agents/hooks.md +122 -0
- package/templates/pi/skills/agents/integration-validator.md +35 -0
- package/templates/pi/skills/agents/issue-creator.md +75 -0
- package/templates/pi/skills/agents/issue-factory.md +143 -0
- package/templates/pi/skills/agents/kanban.md +85 -0
- package/templates/pi/skills/agents/land.md +59 -0
- package/templates/pi/skills/agents/operations-validator.md +43 -0
- package/templates/pi/skills/agents/pipeline.md +113 -0
- package/templates/pi/skills/agents/plan-mode.md +77 -0
- package/templates/pi/skills/agents/pull.md +40 -0
- package/templates/pi/skills/agents/push.md +41 -0
- package/templates/pi/skills/agents/security-validator.md +85 -0
- package/templates/pi/skills/agents/session-persistence.md +119 -0
- package/templates/pi/skills/agents/slash-commands.md +117 -0
- package/templates/pi/skills/agents/snippets.md +99 -0
- package/templates/pi/skills/agents/subagent-registry.md +78 -0
- package/templates/pi/skills/agents/test-validator.md +42 -0
- package/templates/pi/skills/validators/architecture-validator.md +43 -0
- package/templates/pi/skills/validators/ci-validator.md +27 -0
- package/templates/pi/skills/validators/context-compaction.md +73 -0
- package/templates/pi/skills/validators/integration-validator.md +38 -0
- package/templates/pi/skills/validators/model-registry.md +96 -0
- package/templates/pi/skills/validators/operations-validator.md +34 -0
- package/templates/pi/skills/validators/security-guards.md +72 -0
- package/templates/pi/skills/validators/security-validator.md +41 -0
- package/templates/pi/skills/validators/system-prompt-tiers.md +69 -0
- package/templates/pi/skills/validators/test-validator.md +33 -0
- package/templates/pi/types.ts +62 -0
- package/templates/pi/validators/README.md +93 -0
- package/templates/pi/validators/default.toml +209 -0
- package/templates/pi/validators/spring.toml +142 -0
- package/templates/pi/workpad.md +53 -0
- package/templates/project/java/Dockerfile +14 -0
- package/templates/project/java/README.md +40 -0
- package/templates/project/java/pom.xml +132 -0
- package/templates/project/typescript/Dockerfile +15 -0
- package/templates/project/typescript/README.md +29 -0
- package/templates/project/typescript/package.json +23 -0
- package/templates/project/typescript/tsconfig.json +21 -0
- package/templates/workflow.md +123 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redaction Extension for pi
|
|
3
|
+
*
|
|
4
|
+
* Automatically redacts sensitive data from tool results, terminal output,
|
|
5
|
+
* and agent responses. Prevents accidental leakage of API keys, tokens,
|
|
6
|
+
* and credentials in conversation history and exported files.
|
|
7
|
+
*
|
|
8
|
+
* Patterns detected:
|
|
9
|
+
* - OpenAI keys (sk-proj-...)
|
|
10
|
+
* - Anthropic keys (sk-ant-...)
|
|
11
|
+
* - AWS access keys (AKIA...)
|
|
12
|
+
* - GitHub tokens (ghp_, gho_, ghs_, gh_r_, github_pat_)
|
|
13
|
+
* - Google API keys (AIza...)
|
|
14
|
+
* - Slack tokens (xoxb-...)
|
|
15
|
+
* - Stripe keys (sk_live_, pk_live_, sk_test_)
|
|
16
|
+
* - JWTs (eyJ...)
|
|
17
|
+
* - Bearer tokens
|
|
18
|
+
* - Environment variable assignments (API_KEY=..., SECRET=...)
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
22
|
+
|
|
23
|
+
// ── Redaction patterns ──
|
|
24
|
+
|
|
25
|
+
const PATTERNS: Array<{ kind: string; re: RegExp }> = [
|
|
26
|
+
{ kind: "openai-key", re: /\bsk-(?:proj-)?[A-Za-z0-9_-]{20,}\b/g },
|
|
27
|
+
{ kind: "anthropic-key", re: /\bsk-ant-[A-Za-z0-9_-]{20,}\b/g },
|
|
28
|
+
{ kind: "aws-access-key", re: /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/g },
|
|
29
|
+
{ kind: "github-token", re: /\bgh[opsur]_[A-Za-z0-9]{36,}\b/g },
|
|
30
|
+
{ kind: "github-pat", re: /\bgithub_pat_[A-Za-z0-9_]{40,}\b/g },
|
|
31
|
+
{ kind: "google-api-key", re: /\bAIza[0-9A-Za-z_-]{35}\b/g },
|
|
32
|
+
{ kind: "slack-token", re: /\bxox[bpsare]-[A-Za-z0-9-]{10,}\b/g },
|
|
33
|
+
{ kind: "stripe-key", re: /\b(?:sk|pk|rk)_(?:live|test)_[A-Za-z0-9]{24,}\b/g },
|
|
34
|
+
{ kind: "jwt", re: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g },
|
|
35
|
+
{ kind: "bearer", re: /\bBearer\s+[A-Za-z0-9._-]{20,}/g },
|
|
36
|
+
{
|
|
37
|
+
kind: "env-assign",
|
|
38
|
+
re: /\b((?:[A-Z][A-Z0-9_]*)?(?:API[_-]?KEY|SECRET(?:[_-]?KEY)?|ACCESS[_-]?TOKEN|AUTH[_-]?TOKEN|PASSWORD|PASSWD|PRIVATE[_-]?KEY|CLIENT[_-]?SECRET)[A-Z0-9_]*)\s*[:=]\s*(["']?)([^\s"';|&]+)\2/gi,
|
|
39
|
+
},
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Redact sensitive data from text. Returns the redacted string.
|
|
44
|
+
*/
|
|
45
|
+
export function redactSensitive(text: string): string {
|
|
46
|
+
let out = text;
|
|
47
|
+
for (const { kind, re } of PATTERNS) {
|
|
48
|
+
if (kind === "env-assign") {
|
|
49
|
+
out = out.replace(
|
|
50
|
+
re,
|
|
51
|
+
(_m: string, name: string, q: string, _val: string) => `${name}=${q}<REDACTED>${q}`,
|
|
52
|
+
);
|
|
53
|
+
} else {
|
|
54
|
+
out = out.replace(re, `<REDACTED:${kind}>`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Count redactions made (for logging/telemetry-free reporting).
|
|
62
|
+
*/
|
|
63
|
+
export function countRedactions(text: string): number {
|
|
64
|
+
let count = 0;
|
|
65
|
+
for (const { re } of PATTERNS) {
|
|
66
|
+
const matches = text.match(re);
|
|
67
|
+
if (matches) count += matches.length;
|
|
68
|
+
}
|
|
69
|
+
return count;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ── Main extension ──
|
|
73
|
+
|
|
74
|
+
export default function (pi: ExtensionAPI) {
|
|
75
|
+
// Redact sensitive data from tool results before showing to user
|
|
76
|
+
pi.on("tool_result", async (event) => {
|
|
77
|
+
if (!event.result) return;
|
|
78
|
+
|
|
79
|
+
// Redact from text results
|
|
80
|
+
if (typeof event.result === "string") {
|
|
81
|
+
event.result = redactSensitive(event.result);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Redact from structured results
|
|
85
|
+
if (typeof event.result === "object" && event.result !== null) {
|
|
86
|
+
const result = event.result as Record<string, unknown>;
|
|
87
|
+
for (const [key, value] of Object.entries(result)) {
|
|
88
|
+
if (typeof value === "string") {
|
|
89
|
+
result[key] = redactSensitive(value);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// Redact from user input (in case they paste a key)
|
|
96
|
+
pi.on("input", async (event) => {
|
|
97
|
+
const input = (event as { input?: string }).input;
|
|
98
|
+
if (!input) return;
|
|
99
|
+
const redacted = redactSensitive(input);
|
|
100
|
+
if (redacted !== input) {
|
|
101
|
+
const count = countRedactions(event.input) - countRedactions(redacted);
|
|
102
|
+
if (count > 0) {
|
|
103
|
+
event.input = redacted;
|
|
104
|
+
// Note: pi extensions can't easily notify without ctx.ui,
|
|
105
|
+
// but the redaction is applied silently for safety
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Register /redact command for testing
|
|
111
|
+
pi.registerCommand("redact", {
|
|
112
|
+
description: "Test redaction on provided text (for debugging)",
|
|
113
|
+
handler: async (args, ctx) => {
|
|
114
|
+
const text = args.join(" ");
|
|
115
|
+
if (!text) {
|
|
116
|
+
ctx.ui.notify("Usage: /redact <text to test>", "info");
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const redacted = redactSensitive(text);
|
|
120
|
+
const count = countRedactions(text);
|
|
121
|
+
if (count === 0) {
|
|
122
|
+
ctx.ui.notify("No sensitive patterns detected.", "success");
|
|
123
|
+
} else {
|
|
124
|
+
ctx.ui.notify(`Redacted ${count} sensitive pattern(s):\n\n${redacted}`, "warn");
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session Persistence Extension for pi
|
|
3
|
+
*
|
|
4
|
+
* Provides structured session lifecycle with:
|
|
5
|
+
* - Lazy-loaded message history
|
|
6
|
+
* - Auto-derived session titles
|
|
7
|
+
* - Per-session state isolation (todos, read cache)
|
|
8
|
+
* - Atomic JSON storage with temp-file writes
|
|
9
|
+
*
|
|
10
|
+
* Storage: guardian-sessions.json in workspace root
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import * as fs from "node:fs";
|
|
14
|
+
import * as path from "node:path";
|
|
15
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
16
|
+
|
|
17
|
+
// ── Types ──
|
|
18
|
+
|
|
19
|
+
interface SessionMeta {
|
|
20
|
+
id: string;
|
|
21
|
+
title: string;
|
|
22
|
+
createdAt: number;
|
|
23
|
+
updatedAt: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface StoredSession {
|
|
27
|
+
sessions: SessionMeta[];
|
|
28
|
+
activeId: string | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const STORE_FILE = "guardian-sessions.json";
|
|
32
|
+
const MAX_SESSIONS = 50;
|
|
33
|
+
|
|
34
|
+
// ── Storage helpers ──
|
|
35
|
+
|
|
36
|
+
function storePath(cwd: string): string {
|
|
37
|
+
return path.join(cwd, STORE_FILE);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function loadStore(cwd: string): StoredSession {
|
|
41
|
+
const p = storePath(cwd);
|
|
42
|
+
if (!fs.existsSync(p)) {
|
|
43
|
+
return { sessions: [], activeId: null };
|
|
44
|
+
}
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(fs.readFileSync(p, "utf-8")) as StoredSession;
|
|
47
|
+
} catch {
|
|
48
|
+
return { sessions: [], activeId: null };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function saveStore(cwd: string, store: StoredSession): void {
|
|
53
|
+
const p = storePath(cwd);
|
|
54
|
+
const tmp = `${p}.tmp`;
|
|
55
|
+
fs.writeFileSync(tmp, JSON.stringify(store, null, 2), "utf-8");
|
|
56
|
+
fs.renameSync(tmp, p);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function newSessionId(): string {
|
|
60
|
+
const ts = Date.now().toString(36);
|
|
61
|
+
const rand = Math.random().toString(36).slice(2, 8);
|
|
62
|
+
return `s-${ts}-${rand}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Derive title from first user message, stripping context blocks
|
|
66
|
+
function deriveTitle(firstUserMessage: string): string {
|
|
67
|
+
const text = firstUserMessage
|
|
68
|
+
.replace(/<terminal-context[\s\S]*?<\/terminal-context>/g, "")
|
|
69
|
+
.replace(/<selection[\s\S]*?<\/selection>/g, "")
|
|
70
|
+
.replace(/<file[\s\S]*?<\/file>/g, "")
|
|
71
|
+
.replace(/<snippet[\s\S]*?<\/snippet>/g, "")
|
|
72
|
+
.trim();
|
|
73
|
+
if (!text) return "New session";
|
|
74
|
+
const first = text.split("\n")[0].trim();
|
|
75
|
+
return first.length > 40 ? `${first.slice(0, 40)}…` : first;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── Main extension ──
|
|
79
|
+
|
|
80
|
+
export default function (pi: ExtensionAPI) {
|
|
81
|
+
let currentSession: SessionMeta | null = null;
|
|
82
|
+
|
|
83
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
84
|
+
const store = loadStore(ctx.cwd);
|
|
85
|
+
|
|
86
|
+
// Restore active session or create new one
|
|
87
|
+
if (store.activeId) {
|
|
88
|
+
const existing = store.sessions.find((s) => s.id === store.activeId);
|
|
89
|
+
if (existing) {
|
|
90
|
+
currentSession = existing;
|
|
91
|
+
ctx.ui.setStatus("session", `📂 ${existing.title}`);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Create new session
|
|
97
|
+
currentSession = {
|
|
98
|
+
id: newSessionId(),
|
|
99
|
+
title: "New session",
|
|
100
|
+
createdAt: Date.now(),
|
|
101
|
+
updatedAt: Date.now(),
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
store.sessions.unshift(currentSession);
|
|
105
|
+
|
|
106
|
+
// Trim old sessions
|
|
107
|
+
if (store.sessions.length > MAX_SESSIONS) {
|
|
108
|
+
store.sessions = store.sessions.slice(0, MAX_SESSIONS);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
store.activeId = currentSession.id;
|
|
112
|
+
saveStore(ctx.cwd, store);
|
|
113
|
+
|
|
114
|
+
ctx.ui.setStatus("session", `📂 ${currentSession.title}`);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Update session title after first user message
|
|
118
|
+
pi.on("input", async (event, ctx) => {
|
|
119
|
+
if (!currentSession) return;
|
|
120
|
+
if (currentSession.title !== "New session") return;
|
|
121
|
+
|
|
122
|
+
const input = (event as { input?: string }).input;
|
|
123
|
+
if (!input) return;
|
|
124
|
+
const title = deriveTitle(input);
|
|
125
|
+
currentSession.title = title;
|
|
126
|
+
currentSession.updatedAt = Date.now();
|
|
127
|
+
|
|
128
|
+
const store = loadStore(ctx.cwd);
|
|
129
|
+
const idx = store.sessions.findIndex((s) => s.id === currentSession?.id);
|
|
130
|
+
if (idx >= 0) {
|
|
131
|
+
store.sessions[idx] = currentSession;
|
|
132
|
+
saveStore(ctx.cwd, store);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
ctx.ui.setStatus("session", `📂 ${title}`);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Track session activity
|
|
139
|
+
pi.on("tool_result", async (_event, ctx) => {
|
|
140
|
+
if (!currentSession) return;
|
|
141
|
+
currentSession.updatedAt = Date.now();
|
|
142
|
+
|
|
143
|
+
const store = loadStore(ctx.cwd);
|
|
144
|
+
const idx = store.sessions.findIndex((s) => s.id === currentSession?.id);
|
|
145
|
+
if (idx >= 0) {
|
|
146
|
+
store.sessions[idx] = currentSession;
|
|
147
|
+
saveStore(ctx.cwd, store);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// /sessions command — list sessions
|
|
152
|
+
pi.registerCommand("sessions", {
|
|
153
|
+
description: "List all sessions",
|
|
154
|
+
handler: async (_args, ctx) => {
|
|
155
|
+
const store = loadStore(ctx.cwd);
|
|
156
|
+
if (store.sessions.length === 0) {
|
|
157
|
+
ctx.ui.notify("No sessions.", "info");
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const lines = store.sessions.map((s) => {
|
|
161
|
+
const marker = s.id === currentSession?.id ? "▶ " : " ";
|
|
162
|
+
const age = Date.now() - s.updatedAt;
|
|
163
|
+
const ageStr =
|
|
164
|
+
age < 60_000
|
|
165
|
+
? "just now"
|
|
166
|
+
: age < 3_600_000
|
|
167
|
+
? `${Math.floor(age / 60_000)}m ago`
|
|
168
|
+
: `${Math.floor(age / 3_600_000)}h ago`;
|
|
169
|
+
return `${marker}${s.title} (${ageStr})`;
|
|
170
|
+
});
|
|
171
|
+
ctx.ui.notify(`Sessions:\n\n${lines.join("\n")}`, "info");
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slash Commands Extension for pi
|
|
3
|
+
*
|
|
4
|
+
* Intercepts user input starting with `/` and transforms slash commands into
|
|
5
|
+
* structured actions. Supports:
|
|
6
|
+
* /init — Workspace initialization prompt
|
|
7
|
+
* /plan — Plan mode toggle
|
|
8
|
+
* /validate — Run all validators
|
|
9
|
+
* /scope — Classify task scope
|
|
10
|
+
* /snippet — Snippet management (#handle expansion)
|
|
11
|
+
*
|
|
12
|
+
* Also handles `#handle` token expansion for snippets.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import * as fs from "node:fs";
|
|
16
|
+
import * as path from "node:path";
|
|
17
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
18
|
+
|
|
19
|
+
// ── Snippet types ──
|
|
20
|
+
|
|
21
|
+
interface Snippet {
|
|
22
|
+
id: string;
|
|
23
|
+
handle: string;
|
|
24
|
+
name: string;
|
|
25
|
+
description: string;
|
|
26
|
+
content: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const SNIPPETS_FILE = "guardian-snippets.json";
|
|
30
|
+
const HANDLE_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
31
|
+
|
|
32
|
+
function loadSnippets(cwd: string): Snippet[] {
|
|
33
|
+
const snippetsPath = path.join(cwd, SNIPPETS_FILE);
|
|
34
|
+
if (!fs.existsSync(snippetsPath)) return [];
|
|
35
|
+
try {
|
|
36
|
+
return JSON.parse(fs.readFileSync(snippetsPath, "utf-8")) as Snippet[];
|
|
37
|
+
} catch {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function saveSnippets(cwd: string, snippets: Snippet[]): void {
|
|
43
|
+
const tmpPath = path.join(cwd, `${SNIPPETS_FILE}.tmp`);
|
|
44
|
+
const finalPath = path.join(cwd, SNIPPETS_FILE);
|
|
45
|
+
fs.writeFileSync(tmpPath, JSON.stringify(snippets, null, 2), "utf-8");
|
|
46
|
+
fs.renameSync(tmpPath, finalPath);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function newSnippetId(): string {
|
|
50
|
+
return `sn-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function normalizeHandle(raw: string): string {
|
|
54
|
+
return raw
|
|
55
|
+
.trim()
|
|
56
|
+
.toLowerCase()
|
|
57
|
+
.replace(/\s+/g, "-")
|
|
58
|
+
.replace(/[^a-z0-9-]/g, "")
|
|
59
|
+
.replace(/-+/g, "-")
|
|
60
|
+
.replace(/^-+|-+$/g, "");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Expand #handle tokens in text
|
|
64
|
+
function expandSnippetTokens(
|
|
65
|
+
text: string,
|
|
66
|
+
snippets: Snippet[],
|
|
67
|
+
): { body: string; blocks: string[] } {
|
|
68
|
+
const byHandle = new Map(snippets.map((s) => [s.handle, s]));
|
|
69
|
+
const matched = new Map<string, Snippet>();
|
|
70
|
+
|
|
71
|
+
const re = /(^|\s)#([a-z0-9][a-z0-9-]*)\b/gi;
|
|
72
|
+
const body = text.replace(re, (full, lead: string, raw: string) => {
|
|
73
|
+
const h = raw.toLowerCase();
|
|
74
|
+
const snip = byHandle.get(h);
|
|
75
|
+
if (!snip) return full;
|
|
76
|
+
matched.set(snip.id, snip);
|
|
77
|
+
return lead;
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const blocks = Array.from(matched.values()).map(
|
|
81
|
+
(s) => `<snippet name="${s.handle}">\n${s.content}\n</snippet>`,
|
|
82
|
+
);
|
|
83
|
+
return {
|
|
84
|
+
body: body.replace(/[ \t]+\n/g, "\n").trim(),
|
|
85
|
+
blocks,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ── Command definitions ──
|
|
90
|
+
|
|
91
|
+
const INIT_PROMPT = `Scan this workspace and produce a project memory file (.pi/PROJECT.md) with:
|
|
92
|
+
- One-paragraph project description
|
|
93
|
+
- Build / test / dev commands
|
|
94
|
+
- Architecture overview (subsystems, data flow, key dirs)
|
|
95
|
+
- Conventions worth knowing (naming, patterns, gotchas)
|
|
96
|
+
- Paths to entry points
|
|
97
|
+
Cap under 200 lines.`;
|
|
98
|
+
|
|
99
|
+
const VALIDATE_PROMPT = `Run all Guardian validators. For each validator:
|
|
100
|
+
1. Run the script: .pi/scripts/validate-<name>.sh
|
|
101
|
+
2. Report pass/fail with output
|
|
102
|
+
3. If any fail, suggest fixes
|
|
103
|
+
Validators to run: [check manifest for configured validators]`;
|
|
104
|
+
|
|
105
|
+
const SCOPE_PROMPT = `Classify the following task scope:
|
|
106
|
+
- Estimate files affected and lines changed
|
|
107
|
+
- Determine required validators (ci, architecture, security, etc.)
|
|
108
|
+
- Assess risk level (simple, moderate, complex, critical)
|
|
109
|
+
|
|
110
|
+
Task: {task_description}`;
|
|
111
|
+
|
|
112
|
+
// ── Main extension ──
|
|
113
|
+
|
|
114
|
+
export default function (pi: ExtensionAPI) {
|
|
115
|
+
pi.on("session_start", async () => {
|
|
116
|
+
// Load snippets at session start
|
|
117
|
+
const snippets = loadSnippets(process.cwd());
|
|
118
|
+
if (snippets.length > 0) {
|
|
119
|
+
pi.appendEntry({
|
|
120
|
+
key: "guardian:snippets",
|
|
121
|
+
value: snippets.map((s) => `#${s.handle} — ${s.description}`),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Intercept user input for slash commands and snippet expansion
|
|
127
|
+
pi.on("input", async (event, ctx) => {
|
|
128
|
+
const input = (event as { input?: string }).input;
|
|
129
|
+
if (!input) return;
|
|
130
|
+
const trimmed = input.trim();
|
|
131
|
+
|
|
132
|
+
// Handle #handle snippet expansion
|
|
133
|
+
if (trimmed.startsWith("#")) {
|
|
134
|
+
const snippets = loadSnippets(ctx.cwd);
|
|
135
|
+
const { body, blocks } = expandSnippetTokens(input, snippets);
|
|
136
|
+
if (blocks.length > 0) {
|
|
137
|
+
event.input = [...blocks, body].join("\n\n");
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Handle /command
|
|
143
|
+
if (!trimmed.startsWith("/")) return;
|
|
144
|
+
const [cmd, ...args] = trimmed.slice(1).split(/\s+/);
|
|
145
|
+
const argStr = args.join(" ");
|
|
146
|
+
|
|
147
|
+
switch (cmd) {
|
|
148
|
+
case "init":
|
|
149
|
+
event.input = INIT_PROMPT;
|
|
150
|
+
ctx.ui.setStatus("command", "/init — workspace scan");
|
|
151
|
+
break;
|
|
152
|
+
|
|
153
|
+
case "validate":
|
|
154
|
+
event.input = VALIDATE_PROMPT;
|
|
155
|
+
ctx.ui.setStatus("command", "/validate — running validators");
|
|
156
|
+
break;
|
|
157
|
+
|
|
158
|
+
case "scope":
|
|
159
|
+
event.input = SCOPE_PROMPT.replace("{task_description}", argStr || "current task");
|
|
160
|
+
ctx.ui.setStatus("command", "/scope — classifying task");
|
|
161
|
+
break;
|
|
162
|
+
|
|
163
|
+
case "plan":
|
|
164
|
+
// Plan mode is handled by plan-mode.ts extension
|
|
165
|
+
ctx.ui.notify("Plan mode: use the plan-mode extension for /plan commands", "info");
|
|
166
|
+
break;
|
|
167
|
+
|
|
168
|
+
case "snippet": {
|
|
169
|
+
const snippets = loadSnippets(ctx.cwd);
|
|
170
|
+
if (args[0] === "list") {
|
|
171
|
+
if (snippets.length === 0) {
|
|
172
|
+
ctx.ui.notify(
|
|
173
|
+
"No snippets configured. Use /snippet add <handle> to create one.",
|
|
174
|
+
"info",
|
|
175
|
+
);
|
|
176
|
+
} else {
|
|
177
|
+
const list = snippets
|
|
178
|
+
.map((s) => `#${s.handle} — ${s.name}: ${s.description}`)
|
|
179
|
+
.join("\n");
|
|
180
|
+
ctx.ui.notify(`Snippets:\n\n${list}`, "info");
|
|
181
|
+
}
|
|
182
|
+
} else if (args[0] === "add" && args.length >= 2) {
|
|
183
|
+
const handle = normalizeHandle(args[1]);
|
|
184
|
+
if (!HANDLE_RE.test(handle)) {
|
|
185
|
+
ctx.ui.notify(
|
|
186
|
+
`Invalid handle: "${args[1]}". Use lowercase letters, numbers, and hyphens.`,
|
|
187
|
+
"error",
|
|
188
|
+
);
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
const name = args.slice(2).join(" ") || handle;
|
|
192
|
+
const content = await ctx.ui.input("Snippet content", "Enter the snippet body...");
|
|
193
|
+
if (!content) break;
|
|
194
|
+
snippets.push({
|
|
195
|
+
id: newSnippetId(),
|
|
196
|
+
handle,
|
|
197
|
+
name,
|
|
198
|
+
description: name,
|
|
199
|
+
content,
|
|
200
|
+
});
|
|
201
|
+
saveSnippets(ctx.cwd, snippets);
|
|
202
|
+
ctx.ui.notify(`Snippet #${handle} created.`, "success");
|
|
203
|
+
} else if (args[0] === "remove" && args.length >= 2) {
|
|
204
|
+
const handle = normalizeHandle(args[1]);
|
|
205
|
+
const before = snippets.length;
|
|
206
|
+
const filtered = snippets.filter((s) => s.handle !== handle);
|
|
207
|
+
if (filtered.length === before) {
|
|
208
|
+
ctx.ui.notify(`Snippet #${handle} not found.`, "error");
|
|
209
|
+
} else {
|
|
210
|
+
saveSnippets(ctx.cwd, filtered);
|
|
211
|
+
ctx.ui.notify(`Snippet #${handle} removed.`, "success");
|
|
212
|
+
}
|
|
213
|
+
} else {
|
|
214
|
+
ctx.ui.notify(
|
|
215
|
+
"Usage: /snippet list | /snippet add <handle> [name] | /snippet remove <handle>",
|
|
216
|
+
"info",
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
// Don't send to agent — command handled
|
|
220
|
+
event.input = "(snippet command handled — no agent response needed)";
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
default:
|
|
225
|
+
// Unknown command — let it through as normal text
|
|
226
|
+
break;
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
}
|