codehere 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -193
- package/dist/application/use-cases/check-claimed-tests.js +81 -0
- package/dist/application/use-cases/detect-test-weakening.js +237 -0
- package/dist/application/use-cases/rerun-tests.js +110 -7
- package/dist/index.js +38 -1212
- package/dist/infrastructure/orchestration/artifacts-extractor.js +2 -323
- package/dist/presentation/cli/commands/check-command.js +178 -23
- package/dist/presentation/cli/commands/help-command.js +21 -285
- package/dist/ui.js +5 -236
- package/dist/utils/version.js +0 -6
- package/package.json +9 -38
- package/dist/application/services/verdict-service.js +0 -154
- package/dist/application/use-cases/assess-cross-vendor.js +0 -126
- package/dist/application/use-cases/build-cross-vendor-verdict.js +0 -118
- package/dist/application/use-cases/domain-trust.js +0 -110
- package/dist/application/use-cases/export-aat.js +0 -107
- package/dist/application/use-cases/generate-acceptance-criteria.js +0 -223
- package/dist/application/use-cases/generate-final-report.js +0 -259
- package/dist/application/use-cases/generate-narration.js +0 -130
- package/dist/application/use-cases/generate-thread-title.js +0 -181
- package/dist/application/use-cases/narrator-prompt-template.js +0 -158
- package/dist/application/use-cases/plan-auto-review.js +0 -57
- package/dist/application/use-cases/reconcile-audit.js +0 -100
- package/dist/application/use-cases/search-sessions.js +0 -70
- package/dist/application/use-cases/should-auto-review.js +0 -71
- package/dist/application/use-cases/thread-export.js +0 -245
- package/dist/application/use-cases/verify-claim.js +0 -456
- package/dist/application/use-cases/write-pass.js +0 -277
- package/dist/domain/audit.js +0 -11
- package/dist/domain/services/codebase-detector.js +0 -85
- package/dist/domain/task.js +0 -91
- package/dist/error-handler.js +0 -139
- package/dist/infrastructure/completion/bash-completion.js +0 -29
- package/dist/infrastructure/completion/fish-completion.js +0 -17
- package/dist/infrastructure/completion/zsh-completion.js +0 -25
- package/dist/infrastructure/config/config-manager.js +0 -174
- package/dist/infrastructure/config/product-context.js +0 -49
- package/dist/infrastructure/context/adapters/drawio-source.js +0 -157
- package/dist/infrastructure/context/adapters/figma-source.js +0 -198
- package/dist/infrastructure/context/adapters/github-issue-source.js +0 -160
- package/dist/infrastructure/context/adapters/linear-source.js +0 -190
- package/dist/infrastructure/context/context-resolver.js +0 -144
- package/dist/infrastructure/context/context-source.js +0 -114
- package/dist/infrastructure/context/github-api.js +0 -110
- package/dist/infrastructure/context/github-device-flow.js +0 -65
- package/dist/infrastructure/context/oauth-flow.js +0 -207
- package/dist/infrastructure/context/oauth-store.js +0 -122
- package/dist/infrastructure/context/registry-bootstrap.js +0 -95
- package/dist/infrastructure/context/write-pass-resolver.js +0 -106
- package/dist/infrastructure/cost/budget-config.js +0 -114
- package/dist/infrastructure/cost/cost-router.js +0 -97
- package/dist/infrastructure/cost/spend-aggregator.js +0 -222
- package/dist/infrastructure/cost/team-ledger.js +0 -355
- package/dist/infrastructure/discovery/discovery-log.js +0 -85
- package/dist/infrastructure/hooks/audit-tip-signature.js +0 -336
- package/dist/infrastructure/hooks/claude-sdk-hook.js +0 -1018
- package/dist/infrastructure/hooks/hook-mode.js +0 -87
- package/dist/infrastructure/hooks/mirror-tool-call-to-audit.js +0 -49
- package/dist/infrastructure/hooks/pre-prompt-scan.js +0 -122
- package/dist/infrastructure/hooks/session-analyzer.js +0 -177
- package/dist/infrastructure/observability/execution-tracer.js +0 -227
- package/dist/infrastructure/orchestration/agent-adapter.js +0 -13
- package/dist/infrastructure/orchestration/agent-installer.js +0 -101
- package/dist/infrastructure/orchestration/agents-config-seed.js +0 -68
- package/dist/infrastructure/orchestration/aider-adapter.js +0 -611
- package/dist/infrastructure/orchestration/cassette-recorder.js +0 -120
- package/dist/infrastructure/orchestration/claude-code-adapter.js +0 -592
- package/dist/infrastructure/orchestration/codex-adapter.js +0 -574
- package/dist/infrastructure/orchestration/conductor-adapter.js +0 -416
- package/dist/infrastructure/orchestration/context-handoff.js +0 -162
- package/dist/infrastructure/orchestration/control/codex-control-surface.js +0 -99
- package/dist/infrastructure/orchestration/control/control-surface.js +0 -15
- package/dist/infrastructure/orchestration/detect-ollama.js +0 -75
- package/dist/infrastructure/orchestration/failover-classifier.js +0 -151
- package/dist/infrastructure/orchestration/failover-config.js +0 -67
- package/dist/infrastructure/orchestration/failover-policy.js +0 -106
- package/dist/infrastructure/orchestration/gemini-adapter.js +0 -663
- package/dist/infrastructure/orchestration/generic-cli-adapter.js +0 -689
- package/dist/infrastructure/orchestration/graphify-context.js +0 -209
- package/dist/infrastructure/orchestration/memory-injector.js +0 -380
- package/dist/infrastructure/orchestration/mocks/mock-agent-repl.js +0 -388
- package/dist/infrastructure/orchestration/mocks/mock-codex-app-server.js +0 -73
- package/dist/infrastructure/orchestration/mocks/mock-synthesis.js +0 -83
- package/dist/infrastructure/orchestration/mocks/resolve-agent-spawn.js +0 -37
- package/dist/infrastructure/orchestration/opencode-adapter.js +0 -580
- package/dist/infrastructure/orchestration/progress-aggregator.js +0 -906
- package/dist/infrastructure/orchestration/readiness.js +0 -103
- package/dist/infrastructure/orchestration/session-manager.js +0 -1590
- package/dist/infrastructure/orchestration/session-persistence.js +0 -394
- package/dist/infrastructure/orchestration/thread-key-store.js +0 -127
- package/dist/infrastructure/orchestration/write-pass-store.js +0 -38
- package/dist/infrastructure/protocols/cloud-sync.js +0 -207
- package/dist/infrastructure/protocols/mcp-helpers.js +0 -25
- package/dist/infrastructure/protocols/mcp-server-impl.js +0 -3935
- package/dist/infrastructure/protocols/mcp-tools.js +0 -401
- package/dist/infrastructure/protocols/probe-port.js +0 -36
- package/dist/infrastructure/protocols/thread-phase.js +0 -105
- package/dist/infrastructure/protocols/ui/app-script-artifacts.js +0 -121
- package/dist/infrastructure/protocols/ui/app-script-audit-footer.js +0 -44
- package/dist/infrastructure/protocols/ui/app-script-audit-verify.js +0 -56
- package/dist/infrastructure/protocols/ui/app-script-composer-cost.js +0 -588
- package/dist/infrastructure/protocols/ui/app-script-context-sources.js +0 -168
- package/dist/infrastructure/protocols/ui/app-script-conversation.js +0 -67
- package/dist/infrastructure/protocols/ui/app-script-drawer.js +0 -115
- package/dist/infrastructure/protocols/ui/app-script-failover.js +0 -133
- package/dist/infrastructure/protocols/ui/app-script-github.js +0 -113
- package/dist/infrastructure/protocols/ui/app-script-observer.js +0 -104
- package/dist/infrastructure/protocols/ui/app-script-onboarding.js +0 -80
- package/dist/infrastructure/protocols/ui/app-script-overlay.js +0 -203
- package/dist/infrastructure/protocols/ui/app-script-recovery.js +0 -136
- package/dist/infrastructure/protocols/ui/app-script-render.js +0 -166
- package/dist/infrastructure/protocols/ui/app-script-share.js +0 -78
- package/dist/infrastructure/protocols/ui/app-script-sidebar.js +0 -194
- package/dist/infrastructure/protocols/ui/app-script-tour.js +0 -78
- package/dist/infrastructure/protocols/ui/app-script-wpass.js +0 -312
- package/dist/infrastructure/protocols/ui/app-script.js +0 -2998
- package/dist/infrastructure/protocols/ui/font-faces.js +0 -7
- package/dist/infrastructure/protocols/ui/styles.js +0 -2147
- package/dist/infrastructure/protocols/ui/tokens.js +0 -96
- package/dist/infrastructure/protocols/ui-page-v2.js +0 -320
- package/dist/infrastructure/protocols/verify-page.js +0 -1677
- package/dist/infrastructure/security/file-collection.js +0 -118
- package/dist/infrastructure/security/fix-code.js +0 -144
- package/dist/infrastructure/security/fix-secret.js +0 -89
- package/dist/infrastructure/security/permissions-config.js +0 -235
- package/dist/infrastructure/security/scan-types.js +0 -10
- package/dist/infrastructure/security/ship-scan.js +0 -195
- package/dist/infrastructure/security/trust-scorer.js +0 -724
- package/dist/infrastructure/security/vibe-checks.js +0 -99
- package/dist/infrastructure/storage/atomic-write.js +0 -86
- package/dist/infrastructure/storage/cross-tool-agents.js +0 -268
- package/dist/infrastructure/storage/project-thread-migration.js +0 -237
- package/dist/infrastructure/storage/project-thread-mirror.js +0 -671
- package/dist/infrastructure/storage/project-thread-reader.js +0 -409
- package/dist/infrastructure/storage/project-thread-title-backfill.js +0 -214
- package/dist/monitoring.js +0 -212
- package/dist/presentation/cli/commands/audit-command.js +0 -201
- package/dist/presentation/cli/commands/budget-command.js +0 -79
- package/dist/presentation/cli/commands/config-command.js +0 -125
- package/dist/presentation/cli/commands/context-source-command.js +0 -217
- package/dist/presentation/cli/commands/hook-command.js +0 -265
- package/dist/presentation/cli/commands/init-command.js +0 -590
- package/dist/presentation/cli/commands/memory-command.js +0 -1031
- package/dist/presentation/cli/commands/narrator-command.js +0 -101
- package/dist/presentation/cli/commands/permission-command.js +0 -105
- package/dist/presentation/cli/commands/protocols-builder.js +0 -254
- package/dist/presentation/cli/commands/protocols-command.js +0 -84
- package/dist/presentation/cli/commands/repo-map-builder.js +0 -247
- package/dist/presentation/cli/commands/report-command.js +0 -403
- package/dist/presentation/cli/commands/scan-command.js +0 -52
- package/dist/presentation/cli/commands/scan-tool-command.js +0 -110
- package/dist/presentation/cli/commands/search-command.js +0 -51
- package/dist/presentation/cli/commands/setup-command.js +0 -330
- package/dist/presentation/cli/commands/status-command.js +0 -614
- package/dist/presentation/cli/commands/team-command.js +0 -256
- package/dist/presentation/cli/commands/thread-command.js +0 -370
- package/dist/presentation/cli/commands/trust-command.js +0 -328
- package/dist/presentation/cli/commands/verify-command.js +0 -298
- package/dist/presentation/cli/error-display.js +0 -328
- package/dist/presentation/cli/repl.js +0 -805
- package/dist/presentation/cli/task-deliverable.js +0 -153
- package/dist/testing/cassette.js +0 -125
- package/dist/testing/replay-adapter.js +0 -147
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
import { writeFileAtomic } from '../storage/atomic-write.js';
|
|
2
|
-
/**
|
|
3
|
-
* Configuration Manager
|
|
4
|
-
* Manages user preferences and settings for Codehere
|
|
5
|
-
* Stores configuration in data/config.json
|
|
6
|
-
*/
|
|
7
|
-
import { readFileSync, existsSync, mkdirSync } from 'fs';
|
|
8
|
-
import { join } from 'path';
|
|
9
|
-
import { getVersion } from '../../utils/version.js';
|
|
10
|
-
function detectDefaultProvider() {
|
|
11
|
-
if (process.env.ANTHROPIC_API_KEY)
|
|
12
|
-
return { provider: 'anthropic', model: 'claude-fable-5' };
|
|
13
|
-
if (process.env.OPENAI_API_KEY)
|
|
14
|
-
return { provider: 'openai', model: 'gpt-4o-mini' };
|
|
15
|
-
if (process.env.COHERE_API_KEY)
|
|
16
|
-
return { provider: 'cohere', model: 'command-r-08-2024' };
|
|
17
|
-
if (process.env.CODEHERE_AI_PROVIDER === 'local')
|
|
18
|
-
return { provider: 'local', model: 'llama3' };
|
|
19
|
-
return { provider: 'auto', model: 'auto' };
|
|
20
|
-
}
|
|
21
|
-
const DEFAULT_CONFIG = {
|
|
22
|
-
version: getVersion(),
|
|
23
|
-
preferences: {
|
|
24
|
-
autoCommit: false,
|
|
25
|
-
askBeforeDestructive: true,
|
|
26
|
-
enableCrossTeamLearning: false,
|
|
27
|
-
defaultModel: detectDefaultProvider().model,
|
|
28
|
-
maxTokens: 4096,
|
|
29
|
-
temperature: 0.3,
|
|
30
|
-
enableStreaming: true,
|
|
31
|
-
},
|
|
32
|
-
codebase: {},
|
|
33
|
-
api: {
|
|
34
|
-
provider: detectDefaultProvider().provider,
|
|
35
|
-
model: detectDefaultProvider().model,
|
|
36
|
-
rateLimitHandling: 'retry',
|
|
37
|
-
},
|
|
38
|
-
ui: {
|
|
39
|
-
showProgress: true,
|
|
40
|
-
showSummary: true,
|
|
41
|
-
outputFormat: 'full',
|
|
42
|
-
},
|
|
43
|
-
};
|
|
44
|
-
export class ConfigManager {
|
|
45
|
-
configPath;
|
|
46
|
-
config;
|
|
47
|
-
constructor(configDir) {
|
|
48
|
-
const baseDir = configDir || process.cwd();
|
|
49
|
-
this.configPath = join(baseDir, 'data', 'config.json');
|
|
50
|
-
this.config = this.loadConfig();
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* Load configuration from file, creating defaults if not exists
|
|
54
|
-
*/
|
|
55
|
-
loadConfig() {
|
|
56
|
-
// Ensure data directory exists
|
|
57
|
-
const dataDir = join(process.cwd(), 'data');
|
|
58
|
-
if (!existsSync(dataDir)) {
|
|
59
|
-
mkdirSync(dataDir, { recursive: true });
|
|
60
|
-
}
|
|
61
|
-
// Load existing config or use defaults
|
|
62
|
-
if (existsSync(this.configPath)) {
|
|
63
|
-
try {
|
|
64
|
-
const fileContent = readFileSync(this.configPath, 'utf-8');
|
|
65
|
-
const loaded = JSON.parse(fileContent);
|
|
66
|
-
// Merge with defaults to ensure all fields exist
|
|
67
|
-
return {
|
|
68
|
-
...DEFAULT_CONFIG,
|
|
69
|
-
...loaded,
|
|
70
|
-
preferences: {
|
|
71
|
-
...DEFAULT_CONFIG.preferences,
|
|
72
|
-
...loaded.preferences,
|
|
73
|
-
},
|
|
74
|
-
codebase: {
|
|
75
|
-
...DEFAULT_CONFIG.codebase,
|
|
76
|
-
...loaded.codebase,
|
|
77
|
-
},
|
|
78
|
-
api: {
|
|
79
|
-
...DEFAULT_CONFIG.api,
|
|
80
|
-
...loaded.api,
|
|
81
|
-
},
|
|
82
|
-
ui: {
|
|
83
|
-
...DEFAULT_CONFIG.ui,
|
|
84
|
-
...loaded.ui,
|
|
85
|
-
},
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
catch (error) {
|
|
89
|
-
console.warn(`Failed to load config from ${this.configPath}, using defaults:`, error);
|
|
90
|
-
return DEFAULT_CONFIG;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
return DEFAULT_CONFIG;
|
|
94
|
-
}
|
|
95
|
-
/**
|
|
96
|
-
* Save configuration to file
|
|
97
|
-
*/
|
|
98
|
-
saveConfig() {
|
|
99
|
-
try {
|
|
100
|
-
const dataDir = join(process.cwd(), 'data');
|
|
101
|
-
if (!existsSync(dataDir)) {
|
|
102
|
-
mkdirSync(dataDir, { recursive: true });
|
|
103
|
-
}
|
|
104
|
-
writeFileAtomic(this.configPath, JSON.stringify(this.config, null, 2));
|
|
105
|
-
}
|
|
106
|
-
catch (error) {
|
|
107
|
-
throw new Error(`Failed to save config to ${this.configPath}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
/**
|
|
111
|
-
* Get configuration value
|
|
112
|
-
*/
|
|
113
|
-
get(key) {
|
|
114
|
-
return this.config[key];
|
|
115
|
-
}
|
|
116
|
-
/**
|
|
117
|
-
* Get preference value
|
|
118
|
-
*/
|
|
119
|
-
getPreference(key) {
|
|
120
|
-
return this.config.preferences[key];
|
|
121
|
-
}
|
|
122
|
-
/**
|
|
123
|
-
* Set configuration value
|
|
124
|
-
*/
|
|
125
|
-
set(key, value) {
|
|
126
|
-
this.config[key] = value;
|
|
127
|
-
}
|
|
128
|
-
/**
|
|
129
|
-
* Set preference value
|
|
130
|
-
*/
|
|
131
|
-
setPreference(key, value) {
|
|
132
|
-
this.config.preferences[key] = value;
|
|
133
|
-
}
|
|
134
|
-
/**
|
|
135
|
-
* Update codebase information
|
|
136
|
-
*/
|
|
137
|
-
updateCodebase(root, indexedAt) {
|
|
138
|
-
this.config.codebase.root = root;
|
|
139
|
-
if (indexedAt) {
|
|
140
|
-
this.config.codebase.indexedAt = indexedAt;
|
|
141
|
-
}
|
|
142
|
-
this.config.codebase.lastIndexed = new Date().toISOString();
|
|
143
|
-
}
|
|
144
|
-
/**
|
|
145
|
-
* Get full configuration
|
|
146
|
-
*/
|
|
147
|
-
getAll() {
|
|
148
|
-
return { ...this.config };
|
|
149
|
-
}
|
|
150
|
-
/**
|
|
151
|
-
* Reset to defaults
|
|
152
|
-
*/
|
|
153
|
-
reset() {
|
|
154
|
-
this.config = DEFAULT_CONFIG;
|
|
155
|
-
this.saveConfig();
|
|
156
|
-
}
|
|
157
|
-
/**
|
|
158
|
-
* Get config file path
|
|
159
|
-
*/
|
|
160
|
-
getConfigPath() {
|
|
161
|
-
return this.configPath;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
/**
|
|
165
|
-
* Get global config manager instance
|
|
166
|
-
*/
|
|
167
|
-
let globalConfigManager = null;
|
|
168
|
-
export function getConfigManager(configDir) {
|
|
169
|
-
if (!globalConfigManager) {
|
|
170
|
-
globalConfigManager = new ConfigManager(configDir);
|
|
171
|
-
}
|
|
172
|
-
return globalConfigManager;
|
|
173
|
-
}
|
|
174
|
-
//# sourceMappingURL=config-manager.js.map
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Product Context
|
|
3
|
-
*
|
|
4
|
-
* Stores a lightweight "product brain" for the current project.
|
|
5
|
-
* Written by `codehere product init`, read by trust/report/router
|
|
6
|
-
* to give context-aware recommendations.
|
|
7
|
-
*
|
|
8
|
-
* Stored in: .codehere/product.json (per-project, gitignore-friendly)
|
|
9
|
-
*/
|
|
10
|
-
import { readFileSync, existsSync } from 'fs';
|
|
11
|
-
import { join } from 'path';
|
|
12
|
-
const CONFIG_DIR = '.codehere';
|
|
13
|
-
const CONFIG_FILE = 'product.json';
|
|
14
|
-
function getConfigPath(cwd) {
|
|
15
|
-
return join(cwd, CONFIG_DIR, CONFIG_FILE);
|
|
16
|
-
}
|
|
17
|
-
export function loadProductContext(cwd = process.cwd()) {
|
|
18
|
-
const path = getConfigPath(cwd);
|
|
19
|
-
if (!existsSync(path))
|
|
20
|
-
return null;
|
|
21
|
-
try {
|
|
22
|
-
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
23
|
-
}
|
|
24
|
-
catch {
|
|
25
|
-
return null;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Adjusts a trust recommendation based on product context.
|
|
30
|
-
* If no context is available, returns the base recommendation unchanged.
|
|
31
|
-
*/
|
|
32
|
-
export function adjustRecommendation(base, criticalCount, highCount, score, ctx) {
|
|
33
|
-
if (!ctx)
|
|
34
|
-
return base;
|
|
35
|
-
const prefix = ctx.productionApp ? `${ctx.name || 'Production app'} · ` : '';
|
|
36
|
-
if (criticalCount > 0) {
|
|
37
|
-
return `${prefix}${criticalCount} critical issue${criticalCount !== 1 ? 's' : ''} — fix before this goes to production.`;
|
|
38
|
-
}
|
|
39
|
-
if (ctx.riskLevel === 'critical' || ctx.riskLevel === 'high') {
|
|
40
|
-
if (highCount > 0) {
|
|
41
|
-
return `${prefix}${highCount} high-severity pattern${highCount !== 1 ? 's' : ''} — worth fixing before merge given the risk level.`;
|
|
42
|
-
}
|
|
43
|
-
if (score >= 75) {
|
|
44
|
-
return `${prefix}Clean scan. Good to merge — double-check ${ctx.sensitiveAreas.slice(0, 2).join(' and ') || 'sensitive areas'} manually.`;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
return base;
|
|
48
|
-
}
|
|
49
|
-
//# sourceMappingURL=product-context.js.map
|
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* draw.io context-source adapter (v0.8 S5 · third reference adapter).
|
|
3
|
-
*
|
|
4
|
-
* URL shapes accepted (we route both via the same scheme to keep the
|
|
5
|
-
* agent prompt forgiving):
|
|
6
|
-
* - `drawio://<gist-or-raw-url-base64>` · base64url-encoded source URL
|
|
7
|
-
* - `drawio://gh/<owner>/<repo>/<path>` · raw GitHub-hosted .drawio XML
|
|
8
|
-
* - `drawio://url/<urlencoded-https-url>` · arbitrary HTTPS-hosted .drawio
|
|
9
|
-
*
|
|
10
|
-
* No OAuth · draw.io is a client-side editor, the file itself is the
|
|
11
|
-
* payload. We fetch the raw XML, summarize node + edge counts, and emit
|
|
12
|
-
* the first ~3KB of the XML so the agent can read structure without us
|
|
13
|
-
* doing graphviz-style parsing here. Bigger diagrams get a `...truncated`
|
|
14
|
-
* suffix; agents can ask the human for a screenshot if they need pixels.
|
|
15
|
-
*
|
|
16
|
-
* Local-first: no codehere endpoint is contacted. Network call goes
|
|
17
|
-
* directly to GitHub-raw / the supplied HTTPS host. Audit chain records
|
|
18
|
-
* fetch + byte count, never content.
|
|
19
|
-
*/
|
|
20
|
-
import { ContextSourceError, estimateTokens, } from '../context-source.js';
|
|
21
|
-
const MAX_XML_CHARS = 3000;
|
|
22
|
-
const MAX_RESPONSE_BYTES = 1_500_000; // 1.5MB · refuse anything bigger to protect prompt budget
|
|
23
|
-
/** Turn the codehere drawio:// URL into a concrete HTTPS fetch target. */
|
|
24
|
-
function resolveTarget(url) {
|
|
25
|
-
const raw = url.toString();
|
|
26
|
-
const after = raw.slice('drawio://'.length).replace(/^\/+/, '');
|
|
27
|
-
if (!after) {
|
|
28
|
-
throw new ContextSourceError('drawio', 'invalid_url', 'drawio URL missing path');
|
|
29
|
-
}
|
|
30
|
-
// gh/<owner>/<repo>/<path...>
|
|
31
|
-
if (after.startsWith('gh/')) {
|
|
32
|
-
const parts = after.slice('gh/'.length).split('/');
|
|
33
|
-
if (parts.length < 3) {
|
|
34
|
-
throw new ContextSourceError('drawio', 'invalid_url', 'gh/ path needs <owner>/<repo>/<path>');
|
|
35
|
-
}
|
|
36
|
-
const [owner, repo, ...rest] = parts;
|
|
37
|
-
const path = rest.join('/');
|
|
38
|
-
if (!/^[A-Za-z0-9._-]+$/.test(owner) || !/^[A-Za-z0-9._-]+$/.test(repo)) {
|
|
39
|
-
throw new ContextSourceError('drawio', 'invalid_url', 'gh/ owner/repo must be alphanumeric');
|
|
40
|
-
}
|
|
41
|
-
return {
|
|
42
|
-
fetchUrl: `https://raw.githubusercontent.com/${owner}/${repo}/HEAD/${path}`,
|
|
43
|
-
label: `github.com/${owner}/${repo}/${path}`,
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
// url/<urlencoded-https-url>
|
|
47
|
-
if (after.startsWith('url/')) {
|
|
48
|
-
const encoded = after.slice('url/'.length);
|
|
49
|
-
const decoded = safeDecode(encoded);
|
|
50
|
-
if (!/^https:\/\//.test(decoded)) {
|
|
51
|
-
throw new ContextSourceError('drawio', 'invalid_url', 'url/ target must be https://');
|
|
52
|
-
}
|
|
53
|
-
return { fetchUrl: decoded, label: decoded };
|
|
54
|
-
}
|
|
55
|
-
// base64url-encoded full URL
|
|
56
|
-
const decoded = safeBase64UrlDecode(after);
|
|
57
|
-
if (decoded && /^https:\/\//.test(decoded)) {
|
|
58
|
-
return { fetchUrl: decoded, label: decoded };
|
|
59
|
-
}
|
|
60
|
-
throw new ContextSourceError('drawio', 'invalid_url', 'use drawio://gh/<owner>/<repo>/<path> · drawio://url/<urlencoded-https> · or drawio://<base64url-https>');
|
|
61
|
-
}
|
|
62
|
-
function safeDecode(s) {
|
|
63
|
-
try {
|
|
64
|
-
return decodeURIComponent(s);
|
|
65
|
-
}
|
|
66
|
-
catch {
|
|
67
|
-
return '';
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
function safeBase64UrlDecode(s) {
|
|
71
|
-
try {
|
|
72
|
-
const pad = '='.repeat((4 - (s.length % 4)) % 4);
|
|
73
|
-
return Buffer.from(s.replace(/-/g, '+').replace(/_/g, '/') + pad, 'base64').toString('utf-8');
|
|
74
|
-
}
|
|
75
|
-
catch {
|
|
76
|
-
return '';
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
export const drawioSource = {
|
|
80
|
-
id: 'drawio',
|
|
81
|
-
label: 'draw.io',
|
|
82
|
-
scheme: 'drawio',
|
|
83
|
-
description: 'Fetch a draw.io / diagrams.net diagram XML (architecture, flowchart, etc.)',
|
|
84
|
-
isConfigured() {
|
|
85
|
-
// Public-by-URL · no auth needed. Always available.
|
|
86
|
-
return true;
|
|
87
|
-
},
|
|
88
|
-
async fetch(url, ctx) {
|
|
89
|
-
const target = resolveTarget(url);
|
|
90
|
-
const res = await ctx.httpGet(target.fetchUrl, { 'User-Agent': 'codehere/0.8' });
|
|
91
|
-
if (res.status === 404) {
|
|
92
|
-
throw new ContextSourceError('drawio', 'not_found', `diagram not found at ${target.label}`);
|
|
93
|
-
}
|
|
94
|
-
if (res.status === 429) {
|
|
95
|
-
throw new ContextSourceError('drawio', 'rate_limit', `host returned 429 for ${target.label}`);
|
|
96
|
-
}
|
|
97
|
-
if (res.status !== 200) {
|
|
98
|
-
throw new ContextSourceError('drawio', 'network', `host returned ${res.status} for ${target.label}: ${res.body.slice(0, 200)}`);
|
|
99
|
-
}
|
|
100
|
-
if (res.body.length > MAX_RESPONSE_BYTES) {
|
|
101
|
-
throw new ContextSourceError('drawio', 'parse', `diagram too big (${res.body.length} bytes · limit ${MAX_RESPONSE_BYTES})`);
|
|
102
|
-
}
|
|
103
|
-
if (!looksLikeDrawio(res.body)) {
|
|
104
|
-
throw new ContextSourceError('drawio', 'parse', 'response does not look like a draw.io XML or SVG file');
|
|
105
|
-
}
|
|
106
|
-
const stats = quickStats(res.body);
|
|
107
|
-
const text = renderDiagramAsPlainText(target, res.body, stats);
|
|
108
|
-
return {
|
|
109
|
-
url: url.toString(),
|
|
110
|
-
source: 'drawio',
|
|
111
|
-
fetchedAt: new Date(ctx.now ? ctx.now() : Date.now()).toISOString(),
|
|
112
|
-
text,
|
|
113
|
-
estimatedTokens: estimateTokens(text),
|
|
114
|
-
metadata: {
|
|
115
|
-
label: target.label,
|
|
116
|
-
bytes: res.body.length,
|
|
117
|
-
cellCount: stats.cellCount,
|
|
118
|
-
edgeCount: stats.edgeCount,
|
|
119
|
-
pageCount: stats.pageCount,
|
|
120
|
-
},
|
|
121
|
-
};
|
|
122
|
-
},
|
|
123
|
-
};
|
|
124
|
-
/** Does this payload look like a draw.io diagram (XML) or SVG export?
|
|
125
|
-
* Accept any prolog (whitespace, BOM, `<?xml ...?>`) before the first
|
|
126
|
-
* meaningful tag · `<mxfile`, `<mxGraphModel`, or `<svg`. */
|
|
127
|
-
function looksLikeDrawio(body) {
|
|
128
|
-
const trimmed = body.replace(/^\uFEFF/, '').trimStart();
|
|
129
|
-
const stripped = trimmed.startsWith('<?') ? trimmed.slice(trimmed.indexOf('?>') + 2).trimStart() : trimmed;
|
|
130
|
-
return /^<(mxfile|mxGraphModel|svg)\b/i.test(stripped);
|
|
131
|
-
}
|
|
132
|
-
/** Cheap regex stats · we don't ship a real XML parser. */
|
|
133
|
-
function quickStats(xml) {
|
|
134
|
-
const cellMatches = xml.match(/<mxCell\b/g);
|
|
135
|
-
const edgeMatches = xml.match(/<mxCell\b[^>]*\bedge="1"/g);
|
|
136
|
-
const pageMatches = xml.match(/<diagram\b/g);
|
|
137
|
-
return {
|
|
138
|
-
cellCount: cellMatches ? cellMatches.length : 0,
|
|
139
|
-
edgeCount: edgeMatches ? edgeMatches.length : 0,
|
|
140
|
-
pageCount: pageMatches ? pageMatches.length : 1,
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
function renderDiagramAsPlainText(target, xml, stats) {
|
|
144
|
-
const lines = [];
|
|
145
|
-
lines.push(`draw.io diagram · ${target.label}`);
|
|
146
|
-
lines.push('');
|
|
147
|
-
lines.push(`structure: ${stats.cellCount} cells · ${stats.edgeCount} edges · ${stats.pageCount} page${stats.pageCount === 1 ? '' : 's'}`);
|
|
148
|
-
lines.push(`bytes: ${xml.length}`);
|
|
149
|
-
lines.push('');
|
|
150
|
-
lines.push('--- raw xml (truncated) ---');
|
|
151
|
-
lines.push(xml.slice(0, MAX_XML_CHARS));
|
|
152
|
-
if (xml.length > MAX_XML_CHARS) {
|
|
153
|
-
lines.push(`...[truncated · diagram is ${xml.length} chars · ask the human for a screenshot if structure isn't clear]`);
|
|
154
|
-
}
|
|
155
|
-
return lines.join('\n');
|
|
156
|
-
}
|
|
157
|
-
//# sourceMappingURL=drawio-source.js.map
|
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Figma context-source adapter (v0.8 S1 · second reference adapter).
|
|
3
|
-
*
|
|
4
|
-
* URL shapes:
|
|
5
|
-
* figma://file/<file-key> · whole-file metadata + top-level frames
|
|
6
|
-
* figma://file/<file-key>/node/<node-id> · one node + its children, flattened
|
|
7
|
-
*
|
|
8
|
-
* Returns the design tree as indented plain text: frame names, types,
|
|
9
|
-
* dimensions, text content of TEXT nodes, fill colors when they're solid.
|
|
10
|
-
* Generated previews + raster bytes are skipped · agents read text, not
|
|
11
|
-
* pixels (overlay-renderer in v0.9 will surface raw frames separately).
|
|
12
|
-
*
|
|
13
|
-
* Auth: ctx.config['figma'] = { token: '<personal-access-token>' }
|
|
14
|
-
* Token comes from figma.com/developers/api · personal access token.
|
|
15
|
-
*/
|
|
16
|
-
import { ContextSourceError, estimateTokens, } from '../context-source.js';
|
|
17
|
-
const FIGMA_API = 'https://api.figma.com/v1';
|
|
18
|
-
const MAX_TREE_NODES = 200;
|
|
19
|
-
const MAX_TEXT_PER_NODE = 240;
|
|
20
|
-
function parsePath(url) {
|
|
21
|
-
// figma://file/<key> · hostname = 'file', pathname = '/<key>'
|
|
22
|
-
// figma://file/<key>/node/<id>
|
|
23
|
-
const host = url.host || url.hostname; // 'file'
|
|
24
|
-
if (host !== 'file') {
|
|
25
|
-
throw new ContextSourceError('figma', 'invalid_url', `URL must start with figma://file/<key>; got host='${host}'`);
|
|
26
|
-
}
|
|
27
|
-
const parts = url.pathname.split('/').filter(Boolean);
|
|
28
|
-
if (!parts.length) {
|
|
29
|
-
throw new ContextSourceError('figma', 'invalid_url', 'missing file key in figma:// URL');
|
|
30
|
-
}
|
|
31
|
-
const fileKey = parts[0];
|
|
32
|
-
let nodeId = null;
|
|
33
|
-
if (parts.length >= 3 && parts[1] === 'node') {
|
|
34
|
-
nodeId = decodeURIComponent(parts.slice(2).join('/'));
|
|
35
|
-
}
|
|
36
|
-
return { fileKey, nodeId };
|
|
37
|
-
}
|
|
38
|
-
export const figmaSource = {
|
|
39
|
-
id: 'figma',
|
|
40
|
-
label: 'Figma',
|
|
41
|
-
scheme: 'figma',
|
|
42
|
-
description: 'Fetch a Figma file or single node (tree, dimensions, text content)',
|
|
43
|
-
isConfigured(ctx) {
|
|
44
|
-
const cfg = ctx.config['figma'];
|
|
45
|
-
if (!cfg)
|
|
46
|
-
return false;
|
|
47
|
-
if (cfg.oauth?.accessToken)
|
|
48
|
-
return true;
|
|
49
|
-
return typeof cfg.token === 'string' && cfg.token.length > 8;
|
|
50
|
-
},
|
|
51
|
-
async fetch(url, ctx) {
|
|
52
|
-
const cfg = ctx.config['figma'] ?? {};
|
|
53
|
-
// Figma supports BOTH a personal-access token (X-Figma-Token header) AND
|
|
54
|
-
// an OAuth bearer (Authorization: Bearer ...) · pick whichever is set.
|
|
55
|
-
let headers;
|
|
56
|
-
if (cfg.oauth?.accessToken) {
|
|
57
|
-
headers = { Authorization: `Bearer ${cfg.oauth.accessToken}` };
|
|
58
|
-
}
|
|
59
|
-
else if (cfg.token) {
|
|
60
|
-
headers = { 'X-Figma-Token': cfg.token };
|
|
61
|
-
}
|
|
62
|
-
else {
|
|
63
|
-
throw new ContextSourceError('figma', 'auth', 'no token or OAuth in ctx.config["figma"]');
|
|
64
|
-
}
|
|
65
|
-
const { fileKey, nodeId } = parsePath(url);
|
|
66
|
-
const endpoint = cfg.endpoint ?? FIGMA_API;
|
|
67
|
-
let res;
|
|
68
|
-
if (nodeId) {
|
|
69
|
-
const target = `${endpoint}/files/${encodeURIComponent(fileKey)}/nodes?ids=${encodeURIComponent(nodeId)}`;
|
|
70
|
-
res = await ctx.httpGet(target, headers);
|
|
71
|
-
}
|
|
72
|
-
else {
|
|
73
|
-
const target = `${endpoint}/files/${encodeURIComponent(fileKey)}?depth=2`;
|
|
74
|
-
res = await ctx.httpGet(target, headers);
|
|
75
|
-
}
|
|
76
|
-
if (res.status === 401 || res.status === 403) {
|
|
77
|
-
throw new ContextSourceError('figma', 'auth', `Figma returned ${res.status}`);
|
|
78
|
-
}
|
|
79
|
-
if (res.status === 404) {
|
|
80
|
-
throw new ContextSourceError('figma', 'not_found', `file ${fileKey} not found`);
|
|
81
|
-
}
|
|
82
|
-
if (res.status === 429) {
|
|
83
|
-
throw new ContextSourceError('figma', 'rate_limit', 'Figma rate limit hit');
|
|
84
|
-
}
|
|
85
|
-
if (res.status !== 200) {
|
|
86
|
-
throw new ContextSourceError('figma', 'network', `Figma returned ${res.status}: ${res.body.slice(0, 200)}`);
|
|
87
|
-
}
|
|
88
|
-
let text;
|
|
89
|
-
let metadata;
|
|
90
|
-
if (nodeId) {
|
|
91
|
-
const parsed = JSON.parse(res.body);
|
|
92
|
-
if (parsed.err)
|
|
93
|
-
throw new ContextSourceError('figma', 'parse', `Figma error: ${parsed.err}`);
|
|
94
|
-
const node = parsed.nodes?.[nodeId]?.document;
|
|
95
|
-
if (!node)
|
|
96
|
-
throw new ContextSourceError('figma', 'not_found', `node ${nodeId} not in response`);
|
|
97
|
-
text = renderNodeAsTree(node, fileKey, nodeId);
|
|
98
|
-
metadata = { fileKey, nodeId, type: node.type, name: node.name, frames: collectFrames(node) };
|
|
99
|
-
}
|
|
100
|
-
else {
|
|
101
|
-
const parsed = JSON.parse(res.body);
|
|
102
|
-
if (parsed.err)
|
|
103
|
-
throw new ContextSourceError('figma', 'parse', `Figma error: ${parsed.err}`);
|
|
104
|
-
if (!parsed.document)
|
|
105
|
-
throw new ContextSourceError('figma', 'parse', 'Figma response missing document');
|
|
106
|
-
text = renderFile(parsed, fileKey);
|
|
107
|
-
metadata = {
|
|
108
|
-
fileKey,
|
|
109
|
-
name: parsed.name ?? null,
|
|
110
|
-
lastModified: parsed.lastModified ?? null,
|
|
111
|
-
frames: collectFrames(parsed.document),
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
return {
|
|
115
|
-
url: url.toString(),
|
|
116
|
-
source: 'figma',
|
|
117
|
-
fetchedAt: new Date(ctx.now ? ctx.now() : Date.now()).toISOString(),
|
|
118
|
-
text,
|
|
119
|
-
estimatedTokens: estimateTokens(text),
|
|
120
|
-
metadata,
|
|
121
|
-
};
|
|
122
|
-
},
|
|
123
|
-
};
|
|
124
|
-
function collectFrames(root, limit = 12) {
|
|
125
|
-
const frames = [];
|
|
126
|
-
const visit = (node) => {
|
|
127
|
-
if (frames.length >= limit)
|
|
128
|
-
return;
|
|
129
|
-
if (node.type === 'FRAME') {
|
|
130
|
-
const bb = node.absoluteBoundingBox;
|
|
131
|
-
frames.push({
|
|
132
|
-
name: node.name,
|
|
133
|
-
width: bb ? Math.round(bb.width) : null,
|
|
134
|
-
height: bb ? Math.round(bb.height) : null,
|
|
135
|
-
text: firstText(node),
|
|
136
|
-
});
|
|
137
|
-
}
|
|
138
|
-
for (const child of node.children ?? [])
|
|
139
|
-
visit(child);
|
|
140
|
-
};
|
|
141
|
-
visit(root);
|
|
142
|
-
return frames;
|
|
143
|
-
}
|
|
144
|
-
function firstText(node) {
|
|
145
|
-
if (node.characters)
|
|
146
|
-
return node.characters.replace(/\s+/g, ' ').slice(0, MAX_TEXT_PER_NODE);
|
|
147
|
-
for (const child of node.children ?? []) {
|
|
148
|
-
const t = firstText(child);
|
|
149
|
-
if (t)
|
|
150
|
-
return t;
|
|
151
|
-
}
|
|
152
|
-
return null;
|
|
153
|
-
}
|
|
154
|
-
function renderFile(file, fileKey) {
|
|
155
|
-
const lines = [];
|
|
156
|
-
lines.push(`Figma file · ${file.name ?? '(unnamed)'}`);
|
|
157
|
-
lines.push(`https://www.figma.com/file/${fileKey}`);
|
|
158
|
-
if (file.lastModified)
|
|
159
|
-
lines.push(`updated: ${file.lastModified}`);
|
|
160
|
-
lines.push('');
|
|
161
|
-
if (file.document) {
|
|
162
|
-
lines.push('--- top-level tree (depth 2) ---');
|
|
163
|
-
renderInto(lines, file.document, 0, { remaining: MAX_TREE_NODES });
|
|
164
|
-
}
|
|
165
|
-
return lines.join('\n');
|
|
166
|
-
}
|
|
167
|
-
function renderNodeAsTree(node, fileKey, nodeId) {
|
|
168
|
-
const lines = [];
|
|
169
|
-
lines.push(`Figma node · ${node.name} (${node.type})`);
|
|
170
|
-
lines.push(`https://www.figma.com/file/${fileKey}?node-id=${encodeURIComponent(nodeId)}`);
|
|
171
|
-
lines.push('');
|
|
172
|
-
renderInto(lines, node, 0, { remaining: MAX_TREE_NODES });
|
|
173
|
-
return lines.join('\n');
|
|
174
|
-
}
|
|
175
|
-
function renderInto(out, node, depth, budget) {
|
|
176
|
-
if (budget.remaining <= 0) {
|
|
177
|
-
if (budget.remaining === 0) {
|
|
178
|
-
out.push('...[tree truncated · node budget reached]');
|
|
179
|
-
budget.remaining = -1;
|
|
180
|
-
}
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
budget.remaining -= 1;
|
|
184
|
-
const indent = ' '.repeat(depth);
|
|
185
|
-
const bb = node.absoluteBoundingBox;
|
|
186
|
-
const dim = bb ? ` ${Math.round(bb.width)}×${Math.round(bb.height)}` : '';
|
|
187
|
-
const visible = node.visible === false ? ' [hidden]' : '';
|
|
188
|
-
let line = `${indent}${node.type} · ${node.name}${dim}${visible}`;
|
|
189
|
-
if (node.characters) {
|
|
190
|
-
const t = node.characters.replace(/\s+/g, ' ').slice(0, MAX_TEXT_PER_NODE);
|
|
191
|
-
line += ` · "${t}${node.characters.length > MAX_TEXT_PER_NODE ? '…' : ''}"`;
|
|
192
|
-
}
|
|
193
|
-
out.push(line);
|
|
194
|
-
for (const child of node.children ?? []) {
|
|
195
|
-
renderInto(out, child, depth + 1, budget);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
//# sourceMappingURL=figma-source.js.map
|