luca 3.0.0 → 3.0.2
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/.github/workflows/release.yaml +1 -0
- package/CLAUDE.md +10 -2
- package/README.md +130 -112
- package/assistants/codingAssistant/CORE.md +6 -1
- package/assistants/codingAssistant/hooks.ts +1 -1
- package/assistants/inkbot/hooks.ts +1 -1
- package/assistants/inkbot/tools.ts +1 -1
- package/bun.lock +220 -322
- package/commands/audit-docs.ts +2 -2
- package/commands/build-bootstrap.ts +2 -3
- package/commands/build-python-bridge.ts +2 -3
- package/commands/build-scaffolds.ts +2 -3
- package/commands/bundle-consumer-project.ts +521 -0
- package/commands/generate-api-docs.ts +2 -2
- package/commands/inkbot.ts +2 -2
- package/commands/release.ts +2 -2
- package/commands/try-all-challenges.ts +3 -3
- package/commands/try-challenge.ts +3 -3
- package/dist/agi/container.server.d.ts +2 -2
- package/dist/agi/features/assistant.d.ts +2 -2
- package/dist/agi/features/assistants-manager.d.ts +1 -1
- package/dist/agi/features/autonomous-assistant.d.ts +1 -1
- package/dist/agi/features/browser-use.d.ts +1 -1
- package/dist/agi/features/claude-code.d.ts +1 -1
- package/dist/agi/features/conversation-history.d.ts +2 -2
- package/dist/agi/features/conversation.d.ts +1 -1
- package/dist/agi/features/docs-reader.d.ts +1 -1
- package/dist/agi/features/file-tools.d.ts +1 -1
- package/dist/agi/features/luca-coder.d.ts +1 -1
- package/dist/agi/features/openai-codex.d.ts +1 -1
- package/dist/agi/features/skills-library.d.ts +1 -1
- package/dist/clients/civitai/index.d.ts +4 -4
- package/dist/clients/client-template.d.ts +4 -4
- package/dist/clients/comfyui/index.d.ts +2 -2
- package/dist/clients/elevenlabs/index.d.ts +2 -2
- package/dist/clients/openai/index.d.ts +2 -2
- package/dist/clients/supabase/index.d.ts +3 -3
- package/dist/command.d.ts +1 -1
- package/dist/node/container.d.ts +1 -1
- package/dist/node/features/helpers.d.ts +3 -3
- package/dist/node/features/semantic-search.d.ts +1 -1
- package/dist/node/features/vm.d.ts +3 -3
- package/dist/node.d.ts +1 -1
- package/dist/scaffolds/generated.d.ts +1 -1
- package/dist/selector.d.ts +1 -1
- package/index.html +217 -190
- package/luca.console.ts +1 -1
- package/package.json +2 -2
- package/public/index.html +217 -190
- package/public/slides-ai-native.html +1 -1
- package/public/slides-intro.html +2 -2
- package/scripts/examples/ask-luca-expert.ts +1 -1
- package/scripts/examples/assistant-questions.ts +1 -1
- package/scripts/examples/excalidraw-expert.ts +1 -1
- package/scripts/examples/file-manager.ts +1 -1
- package/scripts/examples/ideas.ts +1 -1
- package/scripts/examples/interactive-chat.ts +1 -1
- package/scripts/examples/opening-a-web-browser.ts +1 -1
- package/scripts/examples/telegram-bot.ts +1 -1
- package/scripts/examples/using-assistant-with-mcp.ts +1 -1
- package/scripts/examples/using-claude-code.ts +1 -1
- package/scripts/examples/using-contentdb.ts +2 -2
- package/scripts/examples/using-conversations.ts +1 -1
- package/scripts/examples/using-disk-cache.ts +1 -1
- package/scripts/examples/using-docker-shell.ts +1 -1
- package/scripts/examples/using-elevenlabs.ts +1 -1
- package/scripts/examples/using-google-calendar.ts +1 -1
- package/scripts/examples/using-google-docs.ts +1 -1
- package/scripts/examples/using-google-drive.ts +1 -1
- package/scripts/examples/using-google-sheets.ts +1 -1
- package/scripts/examples/using-nlp.ts +1 -1
- package/scripts/examples/using-ollama.ts +1 -1
- package/scripts/examples/using-postgres.ts +1 -1
- package/scripts/examples/using-runpod.ts +1 -1
- package/scripts/examples/using-tts.ts +1 -1
- package/scripts/scaffold.ts +5 -5
- package/scripts/scratch.ts +1 -1
- package/scripts/test-assistant-hooks.ts +1 -1
- package/scripts/test-docs-reader.ts +1 -1
- package/src/agi/container.server.ts +6 -2
- package/src/agi/features/agent-memory.ts +25 -25
- package/src/agi/features/assistant.ts +34 -5
- package/src/agi/features/assistants-manager.ts +122 -6
- package/src/agi/features/autonomous-assistant.ts +1 -1
- package/src/agi/features/browser-use.ts +20 -1
- package/src/agi/features/claude-code.ts +51 -5
- package/src/agi/features/coding-tools.ts +1 -1
- package/src/agi/features/conversation-history.ts +181 -4
- package/src/agi/features/conversation.ts +186 -15
- package/src/agi/features/docs-reader.ts +2 -2
- package/src/agi/features/file-tools.ts +49 -2
- package/src/agi/features/luca-coder.ts +7 -5
- package/src/agi/features/mcp-bridge.ts +532 -0
- package/src/agi/features/openai-codex.ts +2 -2
- package/src/agi/features/skills-library.ts +131 -52
- package/src/agi/lib/token-counter.ts +80 -0
- package/src/bootstrap/generated.ts +56 -57
- package/src/browser.ts +1 -1
- package/src/cli/build-info.ts +2 -2
- package/src/cli/cli.ts +2 -2
- package/src/clients/civitai/index.ts +5 -5
- package/src/clients/client-template.ts +4 -4
- package/src/clients/comfyui/index.ts +4 -4
- package/src/clients/elevenlabs/index.ts +4 -4
- package/src/clients/openai/index.ts +7 -7
- package/src/clients/supabase/index.ts +4 -4
- package/src/clients/voicebox/index.ts +4 -4
- package/src/command.ts +2 -1
- package/src/commands/chat.ts +1 -0
- package/src/commands/eval.ts +2 -56
- package/src/commands/introspect.ts +1 -1
- package/src/commands/prompt.ts +41 -9
- package/src/container-describer.ts +8 -1
- package/src/container.ts +13 -0
- package/src/entity.ts +2 -2
- package/src/helper.ts +1 -1
- package/src/introspection/generated.agi.ts +28563 -27571
- package/src/introspection/generated.node.ts +20281 -20194
- package/src/introspection/generated.web.ts +605 -584
- package/src/introspection/scan.ts +11 -6
- package/src/node/container.ts +1 -1
- package/src/node/features/content-db.ts +39 -2
- package/src/node/features/display-result.ts +57 -0
- package/src/node/features/helpers.ts +42 -15
- package/src/node/features/python.ts +25 -19
- package/src/node/features/repl.ts +1 -1
- package/src/node/features/secure-shell.ts +11 -17
- package/src/node/features/semantic-search.ts +2 -2
- package/src/node/features/transpiler.ts +2 -3
- package/src/node/features/ui.ts +5 -0
- package/src/node/features/vm.ts +3 -3
- package/src/node.ts +3 -3
- package/src/python/generated.ts +0 -1
- package/src/scaffolds/generated.ts +82 -83
- package/src/selector.ts +1 -1
- package/src/servers/express.ts +1 -1
- package/src/web/features/helpers.ts +22 -0
- package/tsconfig.json +12 -12
- package/docs/CLI.md +0 -335
- package/docs/CNAME +0 -1
- package/docs/README.md +0 -60
- package/docs/TABLE-OF-CONTENTS.md +0 -183
- package/docs/apis/clients/elevenlabs.md +0 -308
- package/docs/apis/clients/graph.md +0 -107
- package/docs/apis/clients/openai.md +0 -429
- package/docs/apis/clients/rest.md +0 -161
- package/docs/apis/clients/websocket.md +0 -174
- package/docs/apis/features/agi/assistant.md +0 -625
- package/docs/apis/features/agi/assistants-manager.md +0 -282
- package/docs/apis/features/agi/auto-assistant.md +0 -279
- package/docs/apis/features/agi/browser-use.md +0 -802
- package/docs/apis/features/agi/claude-code.md +0 -884
- package/docs/apis/features/agi/conversation-history.md +0 -364
- package/docs/apis/features/agi/conversation.md +0 -548
- package/docs/apis/features/agi/docs-reader.md +0 -99
- package/docs/apis/features/agi/file-tools.md +0 -163
- package/docs/apis/features/agi/luca-coder.md +0 -407
- package/docs/apis/features/agi/openai-codex.md +0 -396
- package/docs/apis/features/agi/openapi.md +0 -138
- package/docs/apis/features/agi/semantic-search.md +0 -387
- package/docs/apis/features/agi/skills-library.md +0 -239
- package/docs/apis/features/node/container-link.md +0 -192
- package/docs/apis/features/node/content-db.md +0 -450
- package/docs/apis/features/node/disk-cache.md +0 -379
- package/docs/apis/features/node/dns.md +0 -652
- package/docs/apis/features/node/docker.md +0 -706
- package/docs/apis/features/node/downloader.md +0 -81
- package/docs/apis/features/node/esbuild.md +0 -60
- package/docs/apis/features/node/file-manager.md +0 -191
- package/docs/apis/features/node/fs.md +0 -1217
- package/docs/apis/features/node/git.md +0 -371
- package/docs/apis/features/node/google-auth.md +0 -193
- package/docs/apis/features/node/google-calendar.md +0 -202
- package/docs/apis/features/node/google-docs.md +0 -173
- package/docs/apis/features/node/google-drive.md +0 -246
- package/docs/apis/features/node/google-mail.md +0 -214
- package/docs/apis/features/node/google-sheets.md +0 -194
- package/docs/apis/features/node/grep.md +0 -292
- package/docs/apis/features/node/helpers.md +0 -164
- package/docs/apis/features/node/ink.md +0 -334
- package/docs/apis/features/node/ipc-socket.md +0 -249
- package/docs/apis/features/node/json-tree.md +0 -86
- package/docs/apis/features/node/networking.md +0 -316
- package/docs/apis/features/node/nlp.md +0 -133
- package/docs/apis/features/node/opener.md +0 -97
- package/docs/apis/features/node/os.md +0 -146
- package/docs/apis/features/node/package-finder.md +0 -392
- package/docs/apis/features/node/postgres.md +0 -234
- package/docs/apis/features/node/proc.md +0 -399
- package/docs/apis/features/node/process-manager.md +0 -305
- package/docs/apis/features/node/python.md +0 -604
- package/docs/apis/features/node/redis.md +0 -380
- package/docs/apis/features/node/repl.md +0 -88
- package/docs/apis/features/node/runpod.md +0 -674
- package/docs/apis/features/node/secure-shell.md +0 -176
- package/docs/apis/features/node/semantic-search.md +0 -408
- package/docs/apis/features/node/sqlite.md +0 -233
- package/docs/apis/features/node/telegram.md +0 -279
- package/docs/apis/features/node/transpiler.md +0 -74
- package/docs/apis/features/node/tts.md +0 -133
- package/docs/apis/features/node/ui.md +0 -701
- package/docs/apis/features/node/vault.md +0 -59
- package/docs/apis/features/node/vm.md +0 -75
- package/docs/apis/features/node/yaml-tree.md +0 -85
- package/docs/apis/features/node/yaml.md +0 -176
- package/docs/apis/features/web/asset-loader.md +0 -59
- package/docs/apis/features/web/container-link.md +0 -192
- package/docs/apis/features/web/esbuild.md +0 -54
- package/docs/apis/features/web/helpers.md +0 -164
- package/docs/apis/features/web/network.md +0 -44
- package/docs/apis/features/web/speech.md +0 -69
- package/docs/apis/features/web/vault.md +0 -59
- package/docs/apis/features/web/vm.md +0 -75
- package/docs/apis/features/web/voice.md +0 -84
- package/docs/apis/servers/express.md +0 -171
- package/docs/apis/servers/mcp.md +0 -238
- package/docs/apis/servers/websocket.md +0 -170
- package/docs/bootstrap/CLAUDE.md +0 -101
- package/docs/bootstrap/SKILL.md +0 -341
- package/docs/bootstrap/templates/about-command.ts +0 -41
- package/docs/bootstrap/templates/docs-models.ts +0 -22
- package/docs/bootstrap/templates/docs-readme.md +0 -43
- package/docs/bootstrap/templates/example-feature.ts +0 -53
- package/docs/bootstrap/templates/health-endpoint.ts +0 -15
- package/docs/bootstrap/templates/luca-cli.ts +0 -30
- package/docs/bootstrap/templates/runme.md +0 -54
- package/docs/challenges/caching-proxy.md +0 -16
- package/docs/challenges/content-db-round-trip.md +0 -14
- package/docs/challenges/custom-command.md +0 -9
- package/docs/challenges/file-watcher-pipeline.md +0 -11
- package/docs/challenges/grep-audit-report.md +0 -15
- package/docs/challenges/multi-feature-dashboard.md +0 -14
- package/docs/challenges/process-orchestrator.md +0 -17
- package/docs/challenges/rest-api-server-with-client.md +0 -12
- package/docs/challenges/script-runner-with-vm.md +0 -11
- package/docs/challenges/simple-rest-api.md +0 -15
- package/docs/challenges/websocket-serve-and-client.md +0 -11
- package/docs/challenges/yaml-config-system.md +0 -14
- package/docs/command-system-overhaul.md +0 -94
- package/docs/documentation-audit.md +0 -134
- package/docs/examples/assistant/CORE.md +0 -18
- package/docs/examples/assistant/hooks.ts +0 -3
- package/docs/examples/assistant/tools.ts +0 -10
- package/docs/examples/assistant-hooks-reference.ts +0 -171
- package/docs/examples/assistant-with-process-manager.md +0 -84
- package/docs/examples/content-db.md +0 -77
- package/docs/examples/disk-cache.md +0 -83
- package/docs/examples/docker.md +0 -101
- package/docs/examples/downloader.md +0 -70
- package/docs/examples/entity.md +0 -124
- package/docs/examples/esbuild.md +0 -80
- package/docs/examples/feature-as-tool-provider.md +0 -143
- package/docs/examples/file-manager.md +0 -82
- package/docs/examples/fs.md +0 -83
- package/docs/examples/git.md +0 -85
- package/docs/examples/google-auth.md +0 -88
- package/docs/examples/google-calendar.md +0 -94
- package/docs/examples/google-docs.md +0 -82
- package/docs/examples/google-drive.md +0 -96
- package/docs/examples/google-sheets.md +0 -95
- package/docs/examples/grep.md +0 -85
- package/docs/examples/ink-blocks.md +0 -75
- package/docs/examples/ink-renderer.md +0 -41
- package/docs/examples/ink.md +0 -103
- package/docs/examples/ipc-socket.md +0 -103
- package/docs/examples/json-tree.md +0 -91
- package/docs/examples/networking.md +0 -58
- package/docs/examples/nlp.md +0 -91
- package/docs/examples/opener.md +0 -78
- package/docs/examples/os.md +0 -72
- package/docs/examples/package-finder.md +0 -89
- package/docs/examples/postgres.md +0 -91
- package/docs/examples/proc.md +0 -81
- package/docs/examples/process-manager.md +0 -79
- package/docs/examples/python.md +0 -132
- package/docs/examples/repl.md +0 -93
- package/docs/examples/runpod.md +0 -119
- package/docs/examples/secure-shell.md +0 -92
- package/docs/examples/sqlite.md +0 -86
- package/docs/examples/structured-output-with-assistants.md +0 -144
- package/docs/examples/telegram.md +0 -77
- package/docs/examples/tts.md +0 -86
- package/docs/examples/ui.md +0 -80
- package/docs/examples/vault.md +0 -70
- package/docs/examples/vm.md +0 -86
- package/docs/examples/websocket-ask-and-reply-example.md +0 -128
- package/docs/examples/yaml-tree.md +0 -93
- package/docs/examples/yaml.md +0 -104
- package/docs/ideas/assistant-factory-pattern.md +0 -142
- package/docs/in-memory-fs.md +0 -4
- package/docs/introspection-audit.md +0 -49
- package/docs/introspection.md +0 -164
- package/docs/mcp/readme.md +0 -162
- package/docs/models.ts +0 -41
- package/docs/philosophy.md +0 -86
- package/docs/principles.md +0 -7
- package/docs/prompts/audit-codebase-for-failures-to-use-the-container.md +0 -34
- package/docs/prompts/check-for-undocumented-features.md +0 -27
- package/docs/prompts/mcp-test-easy-command.md +0 -27
- package/docs/scaffolds/client.md +0 -149
- package/docs/scaffolds/command.md +0 -120
- package/docs/scaffolds/endpoint.md +0 -171
- package/docs/scaffolds/feature.md +0 -158
- package/docs/scaffolds/selector.md +0 -91
- package/docs/scaffolds/server.md +0 -196
- package/docs/selectors.md +0 -115
- package/docs/sessions/custom-command/attempt-log-2.md +0 -195
- package/docs/sessions/file-watcher-pipeline/attempt-log-1.md +0 -728
- package/docs/sessions/file-watcher-pipeline/attempt-log-2.md +0 -555
- package/docs/sessions/grep-audit-report/attempt-log-1.md +0 -289
- package/docs/sessions/multi-feature-dashboard/attempt-log-2.md +0 -679
- package/docs/sessions/rest-api-server-with-client/attempt-log-1.md +0 -1
- package/docs/sessions/rest-api-server-with-client/attempt-log-3.md +0 -920
- package/docs/sessions/simple-rest-api/attempt-log-1.md +0 -593
- package/docs/sessions/websocket-serve-and-client/attempt-log-2.md +0 -995
- package/docs/tutorials/00-bootstrap.md +0 -166
- package/docs/tutorials/01-getting-started.md +0 -106
- package/docs/tutorials/02-container.md +0 -210
- package/docs/tutorials/03-scripts.md +0 -194
- package/docs/tutorials/04-features-overview.md +0 -196
- package/docs/tutorials/05-state-and-events.md +0 -171
- package/docs/tutorials/06-servers.md +0 -157
- package/docs/tutorials/07-endpoints.md +0 -198
- package/docs/tutorials/08-commands.md +0 -252
- package/docs/tutorials/09-clients.md +0 -162
- package/docs/tutorials/10-creating-features.md +0 -203
- package/docs/tutorials/11-contentbase.md +0 -191
- package/docs/tutorials/12-assistants.md +0 -215
- package/docs/tutorials/13-introspection.md +0 -157
- package/docs/tutorials/14-type-system.md +0 -174
- package/docs/tutorials/15-project-patterns.md +0 -222
- package/docs/tutorials/16-google-features.md +0 -534
- package/docs/tutorials/17-tui-blocks.md +0 -530
- package/docs/tutorials/18-semantic-search.md +0 -334
- package/docs/tutorials/19-python-sessions.md +0 -401
- package/docs/tutorials/20-browser-esm.md +0 -234
- package/src/agi/endpoints/ask.ts +0 -60
- package/src/agi/endpoints/conversations/[id].ts +0 -45
- package/src/agi/endpoints/conversations.ts +0 -31
- package/src/agi/endpoints/experts.ts +0 -37
- package/test/assistant-hooks.test.ts +0 -306
- package/test/assistant.test.ts +0 -81
- package/test/bus.test.ts +0 -134
- package/test/clients-servers.test.ts +0 -217
- package/test/command.test.ts +0 -267
- package/test/container-link.test.ts +0 -274
- package/test/conversation.test.ts +0 -220
- package/test/features.test.ts +0 -160
- package/test/fork-and-research.test.ts +0 -450
- package/test/integration.test.ts +0 -787
- package/test/interceptor-chain.test.ts +0 -61
- package/test/node-container.test.ts +0 -121
- package/test/python-session.test.ts +0 -105
- package/test/rate-limit.test.ts +0 -272
- package/test/semantic-search.test.ts +0 -550
- package/test/state.test.ts +0 -121
- package/test/vm-context.test.ts +0 -146
- package/test/vm-loadmodule.test.ts +0 -213
- package/test/websocket-ask.test.ts +0 -101
- package/test-integration/assistant.test.ts +0 -138
- package/test-integration/assistants-manager.test.ts +0 -113
- package/test-integration/claude-code.test.ts +0 -98
- package/test-integration/conversation-history.test.ts +0 -205
- package/test-integration/conversation.test.ts +0 -137
- package/test-integration/elevenlabs.test.ts +0 -55
- package/test-integration/google-services.test.ts +0 -80
- package/test-integration/helpers.ts +0 -89
- package/test-integration/memory.test.ts +0 -204
- package/test-integration/openai-codex.test.ts +0 -93
- package/test-integration/runpod.test.ts +0 -58
- package/test-integration/server-endpoints.test.ts +0 -97
- package/test-integration/telegram.test.ts +0 -46
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
import { FeatureStateSchema, FeatureOptionsSchema, FeatureEventsSchema } from '../../schemas/base.js'
|
|
3
|
-
import { type AvailableFeatures } from '
|
|
3
|
+
import { type AvailableFeatures } from 'luca/feature'
|
|
4
4
|
import { Feature } from '../feature.js'
|
|
5
5
|
import type { OpenAIClient } from '../../clients/openai';
|
|
6
6
|
import type OpenAI from 'openai';
|
|
7
7
|
import type { ConversationHistory } from './conversation-history';
|
|
8
|
-
import { countMessageTokens, getContextWindow } from '../lib/token-counter.js';
|
|
8
|
+
import { countMessageTokens, getContextWindow, calculateCost } from '../lib/token-counter.js';
|
|
9
9
|
|
|
10
|
-
declare module '
|
|
10
|
+
declare module 'luca/feature' {
|
|
11
11
|
interface AvailableFeatures {
|
|
12
12
|
conversation: typeof Conversation
|
|
13
13
|
}
|
|
@@ -37,6 +37,20 @@ export interface ConversationMCPServer {
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
const INPUT_TOKEN_SIZES: Record<string, number> = {
|
|
41
|
+
tiny: 8_000,
|
|
42
|
+
small: 16_000,
|
|
43
|
+
medium: 32_000,
|
|
44
|
+
large: 64_000,
|
|
45
|
+
xlarge: 256_000,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function resolveMaxInputTokens(value: number | string | undefined): number | undefined {
|
|
49
|
+
if (value == null) return undefined
|
|
50
|
+
if (typeof value === 'number') return value
|
|
51
|
+
return INPUT_TOKEN_SIZES[value]
|
|
52
|
+
}
|
|
53
|
+
|
|
40
54
|
export const ConversationOptionsSchema = FeatureOptionsSchema.extend({
|
|
41
55
|
/** A unique identifier for the conversation */
|
|
42
56
|
id: z.string().optional().describe('A unique identifier for the conversation'),
|
|
@@ -87,6 +101,12 @@ export const ConversationOptionsSchema = FeatureOptionsSchema.extend({
|
|
|
87
101
|
contextWindow: z.number().optional().describe('Override the inferred context window size for this model'),
|
|
88
102
|
/** Number of recent messages to preserve after compaction (default 4) */
|
|
89
103
|
compactKeepRecent: z.number().optional().describe('Number of recent messages to preserve after compaction (default 4)'),
|
|
104
|
+
|
|
105
|
+
/** Maximum input tokens to send to the API. When set, older messages are trimmed to stay within this budget, keeping the system prompt and most recent messages. Useful for avoiding long-context pricing tiers. Accepts a number or a named size: tiny (8k), small (16k), medium (32k), large (64k), xlarge (256k — max before long-context pricing). */
|
|
106
|
+
maxInputTokens: z.union([
|
|
107
|
+
z.number(),
|
|
108
|
+
z.enum(['tiny', 'small', 'medium', 'large', 'xlarge']),
|
|
109
|
+
]).default('large').describe('Maximum input tokens. Accepts a number or a named size: tiny (8k), small (16k), medium (32k), large (64k), xlarge (256k). Defaults to large (64k)'),
|
|
90
110
|
})
|
|
91
111
|
|
|
92
112
|
export const ConversationStateSchema = FeatureStateSchema.extend({
|
|
@@ -103,7 +123,14 @@ export const ConversationStateSchema = FeatureStateSchema.extend({
|
|
|
103
123
|
prompt: z.number().describe('Total prompt tokens consumed'),
|
|
104
124
|
completion: z.number().describe('Total completion tokens consumed'),
|
|
105
125
|
total: z.number().describe('Total tokens consumed'),
|
|
106
|
-
|
|
126
|
+
cachedTokens: z.number().describe('Input tokens served from cache (billed at reduced rate)'),
|
|
127
|
+
reasoningTokens: z.number().describe('Output tokens used for reasoning (o-series models)'),
|
|
128
|
+
}).describe('Cumulative token usage statistics including detail breakdowns from the API'),
|
|
129
|
+
cost: z.object({
|
|
130
|
+
inputCost: z.number().describe('Estimated cost in dollars for input tokens'),
|
|
131
|
+
outputCost: z.number().describe('Estimated cost in dollars for output tokens'),
|
|
132
|
+
totalCost: z.number().describe('Estimated total cost in dollars'),
|
|
133
|
+
}).describe('Running cost estimate based on cumulative token usage and model pricing'),
|
|
107
134
|
estimatedInputTokens: z.number().describe('Estimated input token count for the current messages array'),
|
|
108
135
|
compactionCount: z.number().describe('Number of times compact() has been called'),
|
|
109
136
|
contextWindow: z.number().describe('The context window size for the current model'),
|
|
@@ -267,7 +294,8 @@ export class Conversation extends Feature<ConversationState, ConversationOptions
|
|
|
267
294
|
toolCalls: 0,
|
|
268
295
|
api: this.apiMode,
|
|
269
296
|
lastResponseId: null,
|
|
270
|
-
tokenUsage: { prompt: 0, completion: 0, total: 0 },
|
|
297
|
+
tokenUsage: { prompt: 0, completion: 0, total: 0, cachedTokens: 0, reasoningTokens: 0 },
|
|
298
|
+
cost: { inputCost: 0, outputCost: 0, totalCost: 0 },
|
|
271
299
|
estimatedInputTokens: 0,
|
|
272
300
|
compactionCount: 0,
|
|
273
301
|
contextWindow: this.options.contextWindow || getContextWindow(this.options.model || 'gpt-5'),
|
|
@@ -598,7 +626,15 @@ export class Conversation extends Feature<ConversationState, ConversationOptions
|
|
|
598
626
|
? messages[0]
|
|
599
627
|
: null
|
|
600
628
|
|
|
601
|
-
|
|
629
|
+
let sliceStart = messages.length - keepRecent
|
|
630
|
+
// Walk back to avoid splitting a tool call group — if we'd start on a tool message,
|
|
631
|
+
// include the preceding assistant message (and its full tool response block)
|
|
632
|
+
if (sliceStart > 0) {
|
|
633
|
+
while (sliceStart > 0 && messages[sliceStart]?.role === 'tool') {
|
|
634
|
+
sliceStart--
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
const recentMessages = messages.slice(sliceStart)
|
|
602
638
|
|
|
603
639
|
const newMessages: Message[] = []
|
|
604
640
|
if (systemMessage) newMessages.push(systemMessage)
|
|
@@ -724,16 +760,20 @@ export class Conversation extends Feature<ConversationState, ConversationOptions
|
|
|
724
760
|
let raw: string
|
|
725
761
|
|
|
726
762
|
if (this.apiMode === 'responses') {
|
|
727
|
-
|
|
763
|
+
// When maxInputTokens is set, skip previous_response_id continuation
|
|
764
|
+
// so we control exactly how many tokens the API processes (server-side
|
|
765
|
+
// context from previous_response_id would accumulate unbounded).
|
|
766
|
+
const canChain = !this.options.maxInputTokens
|
|
767
|
+
const previousResponseId = canChain ? (this.state.get('lastResponseId') || undefined) : undefined
|
|
728
768
|
let input: OpenAI.Responses.ResponseInput
|
|
729
769
|
|
|
730
770
|
if (previousResponseId) {
|
|
731
771
|
// Can chain via previous_response_id — only send the new user message
|
|
732
772
|
input = [this.toResponsesUserMessage(content)]
|
|
733
773
|
} else {
|
|
734
|
-
// No previous response ID (first call
|
|
735
|
-
// Convert
|
|
736
|
-
input = this.messagesToResponsesInput()
|
|
774
|
+
// No previous response ID (first call, resumed from disk, or maxInputTokens active).
|
|
775
|
+
// Convert (possibly trimmed) message history to Responses API input.
|
|
776
|
+
input = this.messagesToResponsesInput(this.getMessagesWithinBudget())
|
|
737
777
|
}
|
|
738
778
|
|
|
739
779
|
raw = await this.runResponsesLoop({
|
|
@@ -816,10 +856,10 @@ export class Conversation extends Feature<ConversationState, ConversationOptions
|
|
|
816
856
|
* Convert the full Chat Completions message history into Responses API input items.
|
|
817
857
|
* Used when resuming a conversation without a previous_response_id.
|
|
818
858
|
*/
|
|
819
|
-
private messagesToResponsesInput(): OpenAI.Responses.ResponseInput {
|
|
859
|
+
private messagesToResponsesInput(messages?: Message[]): OpenAI.Responses.ResponseInput {
|
|
820
860
|
const input: OpenAI.Responses.ResponseInput = []
|
|
821
861
|
|
|
822
|
-
for (const msg of this.messages) {
|
|
862
|
+
for (const msg of (messages || this.messages)) {
|
|
823
863
|
if (msg.role === 'system' || msg.role === 'developer') {
|
|
824
864
|
// System/developer messages are handled via the instructions parameter
|
|
825
865
|
continue
|
|
@@ -917,9 +957,15 @@ export class Conversation extends Feature<ConversationState, ConversationOptions
|
|
|
917
957
|
const lastResponseId = this.state.get('lastResponseId')
|
|
918
958
|
const responseMeta = lastResponseId ? { lastResponseId } : {}
|
|
919
959
|
|
|
960
|
+
// Grab the live token usage and cost from state
|
|
961
|
+
const tokenUsage = this.state.get('tokenUsage')!
|
|
962
|
+
const cost = this.state.get('cost')!
|
|
963
|
+
|
|
920
964
|
if (existing) {
|
|
921
965
|
existing.messages = this.messages
|
|
922
966
|
existing.model = this.model
|
|
967
|
+
existing.tokenUsage = tokenUsage
|
|
968
|
+
existing.cost = cost
|
|
923
969
|
if (opts?.title) existing.title = opts.title
|
|
924
970
|
if (opts?.tags) existing.tags = opts.tags
|
|
925
971
|
if (opts?.thread) existing.thread = opts.thread
|
|
@@ -930,11 +976,13 @@ export class Conversation extends Feature<ConversationState, ConversationOptions
|
|
|
930
976
|
|
|
931
977
|
return this.history.create({
|
|
932
978
|
id,
|
|
933
|
-
title: opts?.title || this.options.title
|
|
979
|
+
title: opts?.title || this.options.title,
|
|
934
980
|
model: this.model,
|
|
935
981
|
messages: this.messages,
|
|
936
982
|
tags: opts?.tags || this.options.tags || [],
|
|
937
983
|
thread: opts?.thread || this.options.thread || this.state.get('thread'),
|
|
984
|
+
tokenUsage,
|
|
985
|
+
cost,
|
|
938
986
|
metadata: { ...responseMeta, ...(opts?.metadata || this.options.metadata || {}) },
|
|
939
987
|
})
|
|
940
988
|
}
|
|
@@ -1163,6 +1211,16 @@ export class Conversation extends Feature<ConversationState, ConversationOptions
|
|
|
1163
1211
|
return accumulated || finalText
|
|
1164
1212
|
}
|
|
1165
1213
|
|
|
1214
|
+
/** Recalculate the running cost estimate from current token usage and update state. */
|
|
1215
|
+
private updateCost() {
|
|
1216
|
+
const tokenUsage = this.state.get('tokenUsage')!
|
|
1217
|
+
const { inputCost, outputCost, totalCost } = calculateCost(this.model, tokenUsage.prompt, tokenUsage.completion, {
|
|
1218
|
+
cachedTokens: tokenUsage.cachedTokens,
|
|
1219
|
+
reasoningTokens: tokenUsage.reasoningTokens,
|
|
1220
|
+
})
|
|
1221
|
+
this.state.set('cost', { inputCost, outputCost, totalCost })
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1166
1224
|
/** Apply Responses API usage stats to this conversation's token usage counters. */
|
|
1167
1225
|
private applyResponsesUsage(usage?: OpenAI.Responses.ResponseUsage) {
|
|
1168
1226
|
if (!usage) return
|
|
@@ -1171,7 +1229,10 @@ export class Conversation extends Feature<ConversationState, ConversationOptions
|
|
|
1171
1229
|
prompt: prev.prompt + (usage.input_tokens || 0),
|
|
1172
1230
|
completion: prev.completion + (usage.output_tokens || 0),
|
|
1173
1231
|
total: prev.total + (usage.total_tokens || 0),
|
|
1232
|
+
cachedTokens: prev.cachedTokens + (usage.input_tokens_details?.cached_tokens || 0),
|
|
1233
|
+
reasoningTokens: prev.reasoningTokens + (usage.output_tokens_details?.reasoning_tokens || 0),
|
|
1174
1234
|
})
|
|
1235
|
+
this.updateCost()
|
|
1175
1236
|
}
|
|
1176
1237
|
|
|
1177
1238
|
/**
|
|
@@ -1208,7 +1269,7 @@ export class Conversation extends Feature<ConversationState, ConversationOptions
|
|
|
1208
1269
|
try {
|
|
1209
1270
|
const stream = await this.openai.raw.chat.completions.create({
|
|
1210
1271
|
model: this.model,
|
|
1211
|
-
messages: this.
|
|
1272
|
+
messages: this.sanitizeMessages(this.getMessagesWithinBudget()),
|
|
1212
1273
|
stream: true,
|
|
1213
1274
|
...(toolsParam ? { tools: toolsParam, tool_choice: 'auto' } : {}),
|
|
1214
1275
|
...(this.maxTokens ? { [this.maxTokensParam]: this.maxTokens } : {}),
|
|
@@ -1258,8 +1319,11 @@ export class Conversation extends Feature<ConversationState, ConversationOptions
|
|
|
1258
1319
|
this.state.set('tokenUsage', {
|
|
1259
1320
|
prompt: prev.prompt + (chunk.usage.prompt_tokens || 0),
|
|
1260
1321
|
completion: prev.completion + (chunk.usage.completion_tokens || 0),
|
|
1261
|
-
total: prev.total + (chunk.usage.total_tokens || 0)
|
|
1322
|
+
total: prev.total + (chunk.usage.total_tokens || 0),
|
|
1323
|
+
cachedTokens: prev.cachedTokens + (chunk.usage.prompt_tokens_details?.cached_tokens || 0),
|
|
1324
|
+
reasoningTokens: prev.reasoningTokens + (chunk.usage.completion_tokens_details?.reasoning_tokens || 0),
|
|
1262
1325
|
})
|
|
1326
|
+
this.updateCost()
|
|
1263
1327
|
}
|
|
1264
1328
|
}
|
|
1265
1329
|
} finally {
|
|
@@ -1310,6 +1374,113 @@ export class Conversation extends Feature<ConversationState, ConversationOptions
|
|
|
1310
1374
|
return accumulated
|
|
1311
1375
|
}
|
|
1312
1376
|
|
|
1377
|
+
/**
|
|
1378
|
+
* Returns the messages array trimmed to fit within the maxInputTokens budget.
|
|
1379
|
+
* Keeps the system/developer message and drops oldest atomic groups first.
|
|
1380
|
+
*
|
|
1381
|
+
* Messages are grouped into atomic units so tool call/response pairs are never
|
|
1382
|
+
* split (which would cause a 400 from OpenAI):
|
|
1383
|
+
* - assistant with tool_calls + its subsequent tool response messages = one group
|
|
1384
|
+
* - standalone user, assistant (no tools), system = one group each
|
|
1385
|
+
*
|
|
1386
|
+
* If no maxInputTokens is set, returns messages as-is.
|
|
1387
|
+
*/
|
|
1388
|
+
private getMessagesWithinBudget(): Message[] {
|
|
1389
|
+
const budget = resolveMaxInputTokens(this.options.maxInputTokens)
|
|
1390
|
+
if (!budget) return this.messages
|
|
1391
|
+
|
|
1392
|
+
const messages = this.messages
|
|
1393
|
+
if (messages.length === 0) return messages
|
|
1394
|
+
|
|
1395
|
+
// Check if the full history already fits
|
|
1396
|
+
const fullCount = countMessageTokens(messages, this.model)
|
|
1397
|
+
if (fullCount <= budget) return messages
|
|
1398
|
+
|
|
1399
|
+
// Separate system prompt from the rest
|
|
1400
|
+
const systemMsg = (messages[0]?.role === 'system' || messages[0]?.role === 'developer')
|
|
1401
|
+
? messages[0]
|
|
1402
|
+
: null
|
|
1403
|
+
const nonSystem = systemMsg ? messages.slice(1) : [...messages]
|
|
1404
|
+
|
|
1405
|
+
// Group messages into atomic units.
|
|
1406
|
+
// An assistant message with tool_calls and its subsequent tool responses form one group.
|
|
1407
|
+
type MessageGroup = Message[]
|
|
1408
|
+
const groups: MessageGroup[] = []
|
|
1409
|
+
let i = 0
|
|
1410
|
+
while (i < nonSystem.length) {
|
|
1411
|
+
const msg = nonSystem[i]!
|
|
1412
|
+
if (msg.role === 'assistant' && (msg as any).tool_calls?.length) {
|
|
1413
|
+
// Collect the assistant + all following tool responses that belong to it
|
|
1414
|
+
const expectedIds = new Set(((msg as any).tool_calls as any[]).map((tc: any) => tc.id))
|
|
1415
|
+
const group: Message[] = [msg]
|
|
1416
|
+
let j = i + 1
|
|
1417
|
+
while (j < nonSystem.length && nonSystem[j]!.role === 'tool' && expectedIds.has((nonSystem[j] as any).tool_call_id)) {
|
|
1418
|
+
group.push(nonSystem[j]!)
|
|
1419
|
+
j++
|
|
1420
|
+
}
|
|
1421
|
+
groups.push(group)
|
|
1422
|
+
i = j
|
|
1423
|
+
} else {
|
|
1424
|
+
groups.push([msg])
|
|
1425
|
+
i++
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
// Walk backwards through groups, accumulating tokens until we exceed the budget
|
|
1430
|
+
const systemTokens = systemMsg ? countMessageTokens([systemMsg], this.model) : 0
|
|
1431
|
+
let running = systemTokens
|
|
1432
|
+
let cutoff = groups.length // start with nothing included
|
|
1433
|
+
|
|
1434
|
+
for (let g = groups.length - 1; g >= 0; g--) {
|
|
1435
|
+
const groupTokens = countMessageTokens(groups[g]!, this.model)
|
|
1436
|
+
if (running + groupTokens > budget) break
|
|
1437
|
+
running += groupTokens
|
|
1438
|
+
cutoff = g
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
const kept = groups.slice(cutoff).flat()
|
|
1442
|
+
return systemMsg ? [systemMsg, ...kept] : kept
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
private sanitizeMessages(messages: Message[]): Message[] {
|
|
1446
|
+
const result: Message[] = []
|
|
1447
|
+
|
|
1448
|
+
for (let i = 0; i < messages.length; i++) {
|
|
1449
|
+
const msg = messages[i]!
|
|
1450
|
+
result.push(msg)
|
|
1451
|
+
|
|
1452
|
+
// Check if this is an assistant message with tool_calls
|
|
1453
|
+
if (msg.role === 'assistant' && (msg as any).tool_calls?.length) {
|
|
1454
|
+
const toolCalls: Array<{ id: string }> = (msg as any).tool_calls
|
|
1455
|
+
const expectedIds = new Set(toolCalls.map(tc => tc.id))
|
|
1456
|
+
|
|
1457
|
+
// Scan forward for matching tool responses
|
|
1458
|
+
const foundIds = new Set<string>()
|
|
1459
|
+
for (let j = i + 1; j < messages.length; j++) {
|
|
1460
|
+
const next = messages[j]!
|
|
1461
|
+
if (next.role === 'tool' && expectedIds.has((next as any).tool_call_id)) {
|
|
1462
|
+
foundIds.add((next as any).tool_call_id)
|
|
1463
|
+
} else if (next.role !== 'tool') {
|
|
1464
|
+
break
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
// Add stub responses for any missing tool_call_ids
|
|
1469
|
+
for (const id of expectedIds) {
|
|
1470
|
+
if (!foundIds.has(id)) {
|
|
1471
|
+
result.push({
|
|
1472
|
+
role: 'tool',
|
|
1473
|
+
tool_call_id: id,
|
|
1474
|
+
content: '[tool execution was interrupted]',
|
|
1475
|
+
} as any)
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
return result
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1313
1484
|
/**
|
|
1314
1485
|
* Append a message to the conversation state.
|
|
1315
1486
|
*
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
2
|
import { FeatureStateSchema, FeatureOptionsSchema, FeatureEventsSchema } from '../../schemas/base.js'
|
|
3
|
-
import { type AvailableFeatures } from '
|
|
3
|
+
import { type AvailableFeatures } from 'luca/feature'
|
|
4
4
|
import { Feature } from '../feature.js'
|
|
5
5
|
import type { ContentDb } from '@/node.js'
|
|
6
6
|
import type Assistant from './assistant.js'
|
|
7
7
|
|
|
8
|
-
declare module '
|
|
8
|
+
declare module 'luca/feature' {
|
|
9
9
|
interface AvailableFeatures {
|
|
10
10
|
docsReader: typeof DocsReader
|
|
11
11
|
}
|
|
@@ -5,14 +5,17 @@ import type { FS } from '../../node/features/fs.js'
|
|
|
5
5
|
import type { Grep, GrepMatch } from '../../node/features/grep.js'
|
|
6
6
|
import type { Helper } from '../../helper.js'
|
|
7
7
|
|
|
8
|
-
declare module '
|
|
8
|
+
declare module 'luca/feature' {
|
|
9
9
|
interface AvailableFeatures {
|
|
10
10
|
fileTools: typeof FileTools
|
|
11
11
|
}
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export const FileToolsStateSchema = FeatureStateSchema.extend({})
|
|
15
|
-
export const FileToolsOptionsSchema = FeatureOptionsSchema.extend({
|
|
15
|
+
export const FileToolsOptionsSchema = FeatureOptionsSchema.extend({
|
|
16
|
+
lockToFolder: z.string().optional().describe('When set, all file operations are restricted to this folder. Paths outside it are rejected.'),
|
|
17
|
+
forbid: z.array(z.union([z.string(), z.instanceof(RegExp)])).optional().describe('Patterns (strings or RegExps) that block access to any matching path.'),
|
|
18
|
+
})
|
|
16
19
|
|
|
17
20
|
/**
|
|
18
21
|
* Curated file-system and code-search tools for AI assistants.
|
|
@@ -133,11 +136,43 @@ export class FileTools extends Feature {
|
|
|
133
136
|
return this.container.feature('grep') as unknown as Grep
|
|
134
137
|
}
|
|
135
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Resolve a user-supplied path to an absolute path and validate it against
|
|
141
|
+
* `lockToFolder` and `forbid` constraints. Throws if the path is blocked.
|
|
142
|
+
*/
|
|
143
|
+
private validatePath(inputPath: string): string {
|
|
144
|
+
const resolved = this.container.paths.resolve(inputPath)
|
|
145
|
+
|
|
146
|
+
const { lockToFolder, forbid } = this.options as { lockToFolder?: string; forbid?: (string | RegExp)[] }
|
|
147
|
+
|
|
148
|
+
if (lockToFolder) {
|
|
149
|
+
const folder = this.container.paths.resolve(lockToFolder)
|
|
150
|
+
// The resolved path must be inside the locked folder (or be the folder itself)
|
|
151
|
+
if (resolved !== folder && !resolved.startsWith(folder + '/')) {
|
|
152
|
+
throw new Error(`Access denied: "${inputPath}" is outside the allowed folder "${lockToFolder}"`)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (forbid && forbid.length) {
|
|
157
|
+
for (const pattern of forbid) {
|
|
158
|
+
const matches = pattern instanceof RegExp
|
|
159
|
+
? pattern.test(resolved)
|
|
160
|
+
: resolved.includes(pattern)
|
|
161
|
+
if (matches) {
|
|
162
|
+
throw new Error(`Access denied: "${inputPath}" matches forbidden pattern "${pattern}"`)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return resolved
|
|
168
|
+
}
|
|
169
|
+
|
|
136
170
|
// -------------------------------------------------------------------------
|
|
137
171
|
// Tool implementations — each matches a static tools key by name
|
|
138
172
|
// -------------------------------------------------------------------------
|
|
139
173
|
|
|
140
174
|
async readFile(args: { path: string; offset?: number; limit?: number }): Promise<string> {
|
|
175
|
+
this.validatePath(args.path)
|
|
141
176
|
const content = await this.fs.readFileAsync(args.path) as string
|
|
142
177
|
|
|
143
178
|
if (args.offset || args.limit) {
|
|
@@ -151,12 +186,14 @@ export class FileTools extends Feature {
|
|
|
151
186
|
}
|
|
152
187
|
|
|
153
188
|
async writeFile(args: { path: string; content: string }): Promise<string> {
|
|
189
|
+
this.validatePath(args.path)
|
|
154
190
|
await this.fs.ensureFolderAsync(args.path.includes('/') ? args.path.split('/').slice(0, -1).join('/') : '.')
|
|
155
191
|
await this.fs.writeFileAsync(args.path, args.content)
|
|
156
192
|
return `Wrote ${args.content.length} bytes to ${args.path}`
|
|
157
193
|
}
|
|
158
194
|
|
|
159
195
|
async editFile(args: { path: string; oldString: string; newString: string; replaceAll?: boolean }): Promise<string> {
|
|
196
|
+
this.validatePath(args.path)
|
|
160
197
|
const content = await this.fs.readFileAsync(args.path) as string
|
|
161
198
|
|
|
162
199
|
if (args.replaceAll) {
|
|
@@ -183,6 +220,7 @@ export class FileTools extends Feature {
|
|
|
183
220
|
|
|
184
221
|
async listDirectory(args: { path?: string; recursive?: boolean; include?: string; exclude?: string }): Promise<string> {
|
|
185
222
|
const dir = args.path || '.'
|
|
223
|
+
this.validatePath(dir)
|
|
186
224
|
const result = await this.fs.walkAsync(dir, {
|
|
187
225
|
files: true,
|
|
188
226
|
directories: true,
|
|
@@ -201,6 +239,7 @@ export class FileTools extends Feature {
|
|
|
201
239
|
}
|
|
202
240
|
|
|
203
241
|
async searchFiles(args: { pattern: string; path?: string; include?: string; exclude?: string; ignoreCase?: boolean; maxResults?: number }): Promise<string> {
|
|
242
|
+
if (args.path) this.validatePath(args.path)
|
|
204
243
|
const results: GrepMatch[] = await this.grep.search({
|
|
205
244
|
pattern: args.pattern,
|
|
206
245
|
path: args.path,
|
|
@@ -219,6 +258,7 @@ export class FileTools extends Feature {
|
|
|
219
258
|
|
|
220
259
|
async findFiles(args: { pattern: string; path?: string; exclude?: string }): Promise<string> {
|
|
221
260
|
const dir = args.path || '.'
|
|
261
|
+
this.validatePath(dir)
|
|
222
262
|
const result = await this.fs.walkAsync(dir, {
|
|
223
263
|
files: true,
|
|
224
264
|
directories: false,
|
|
@@ -230,6 +270,7 @@ export class FileTools extends Feature {
|
|
|
230
270
|
}
|
|
231
271
|
|
|
232
272
|
async fileInfo(args: { path: string }): Promise<string> {
|
|
273
|
+
this.validatePath(args.path)
|
|
233
274
|
const exists = await this.fs.existsAsync(args.path)
|
|
234
275
|
if (!exists) return JSON.stringify({ exists: false })
|
|
235
276
|
|
|
@@ -244,21 +285,27 @@ export class FileTools extends Feature {
|
|
|
244
285
|
}
|
|
245
286
|
|
|
246
287
|
async createDirectory(args: { path: string }): Promise<string> {
|
|
288
|
+
this.validatePath(args.path)
|
|
247
289
|
await this.fs.ensureFolderAsync(args.path)
|
|
248
290
|
return `Created ${args.path}`
|
|
249
291
|
}
|
|
250
292
|
|
|
251
293
|
async moveFile(args: { source: string; destination: string }): Promise<string> {
|
|
294
|
+
this.validatePath(args.source)
|
|
295
|
+
this.validatePath(args.destination)
|
|
252
296
|
await this.fs.moveAsync(args.source, args.destination)
|
|
253
297
|
return `Moved ${args.source} → ${args.destination}`
|
|
254
298
|
}
|
|
255
299
|
|
|
256
300
|
async copyFile(args: { source: string; destination: string }): Promise<string> {
|
|
301
|
+
this.validatePath(args.source)
|
|
302
|
+
this.validatePath(args.destination)
|
|
257
303
|
await this.fs.copyAsync(args.source, args.destination)
|
|
258
304
|
return `Copied ${args.source} → ${args.destination}`
|
|
259
305
|
}
|
|
260
306
|
|
|
261
307
|
async deleteFile(args: { path: string }): Promise<string> {
|
|
308
|
+
this.validatePath(args.path)
|
|
262
309
|
const isDir = await this.fs.isDirectoryAsync(args.path)
|
|
263
310
|
if (isDir) return `Error: "${args.path}" is a directory. Use deleteFile only for files.`
|
|
264
311
|
await this.fs.rm(args.path)
|
|
@@ -4,7 +4,7 @@ import { Feature } from '../feature.js'
|
|
|
4
4
|
import type { Assistant } from './assistant.js'
|
|
5
5
|
import type { ToolCallCtx } from '../lib/interceptor-chain.js'
|
|
6
6
|
|
|
7
|
-
declare module '
|
|
7
|
+
declare module 'luca/feature' {
|
|
8
8
|
interface AvailableFeatures {
|
|
9
9
|
lucaCoder: typeof LucaCoder
|
|
10
10
|
}
|
|
@@ -104,8 +104,8 @@ export const LucaCoderOptionsSchema = FeatureOptionsSchema.extend({
|
|
|
104
104
|
/** Skills to auto-load into the system prompt context. If not specified, auto-detects luca-framework. */
|
|
105
105
|
skills: z.array(z.string()).optional().describe('Skill names to auto-load into the system prompt'),
|
|
106
106
|
|
|
107
|
-
/** Whether to auto-detect and load the luca-framework skill. Defaults to true. */
|
|
108
|
-
autoLoadLucaSkill: z.boolean().default(true).describe('Auto-load luca-framework skill if found in .claude/skills
|
|
107
|
+
/** Whether to auto-detect and load the luca-framework skill from conventional agent skill folders. Defaults to true. */
|
|
108
|
+
autoLoadLucaSkill: z.boolean().default(true).describe('Auto-load luca-framework skill if found in agent skill folders (.claude/skills or .agents/skills)'),
|
|
109
109
|
})
|
|
110
110
|
|
|
111
111
|
export type LucaCoderState = z.infer<typeof LucaCoderStateSchema>
|
|
@@ -116,7 +116,7 @@ export type LucaCoderOptions = z.infer<typeof LucaCoderOptionsSchema>
|
|
|
116
116
|
* gates all tool calls through a permission system.
|
|
117
117
|
*
|
|
118
118
|
* Comes with built-in Bash tool (via proc.execAndCapture) and auto-loads
|
|
119
|
-
* the luca-framework skill when found in .claude/skills
|
|
119
|
+
* the luca-framework skill when found in conventional agent skill folders (.claude/skills or .agents/skills).
|
|
120
120
|
*
|
|
121
121
|
* Tools are stacked from feature bundles (fileTools, etc.)
|
|
122
122
|
* and each tool can be set to 'allow' (runs immediately), 'ask' (blocks
|
|
@@ -415,11 +415,13 @@ export class LucaCoder extends Feature<LucaCoderState, LucaCoderOptions> {
|
|
|
415
415
|
const skillContent: string[] = []
|
|
416
416
|
const loadedSkills: string[] = []
|
|
417
417
|
|
|
418
|
-
// Check for luca-framework skill in
|
|
418
|
+
// Check for luca-framework skill in conventional agent skill folders
|
|
419
419
|
if (this.options.autoLoadLucaSkill !== false) {
|
|
420
420
|
const skillLocations = [
|
|
421
421
|
paths.resolve(this.container.cwd, '.claude', 'skills', 'luca-framework', 'SKILL.md'),
|
|
422
|
+
paths.resolve(this.container.cwd, '.agents', 'skills', 'luca-framework', 'SKILL.md'),
|
|
422
423
|
paths.resolve(os.homedir, '.claude', 'skills', 'luca-framework', 'SKILL.md'),
|
|
424
|
+
paths.resolve(os.homedir, '.agents', 'skills', 'luca-framework', 'SKILL.md'),
|
|
423
425
|
]
|
|
424
426
|
|
|
425
427
|
for (const skillPath of skillLocations) {
|