@greppleai/grip 0.1.6 → 0.1.7

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.
Files changed (76) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +1 -1
  3. package/dist/config.d.ts.map +1 -1
  4. package/dist/config.js +2 -0
  5. package/dist/config.js.map +1 -1
  6. package/dist/core/agent-session.d.ts +11 -1
  7. package/dist/core/agent-session.d.ts.map +1 -1
  8. package/dist/core/agent-session.js +172 -2
  9. package/dist/core/agent-session.js.map +1 -1
  10. package/dist/core/compaction/compaction.d.ts.map +1 -1
  11. package/dist/core/compaction/compaction.js +9 -2
  12. package/dist/core/compaction/compaction.js.map +1 -1
  13. package/dist/core/extensions/runner.d.ts +3 -0
  14. package/dist/core/extensions/runner.d.ts.map +1 -1
  15. package/dist/core/extensions/runner.js +22 -0
  16. package/dist/core/extensions/runner.js.map +1 -1
  17. package/dist/core/extensions/types.d.ts +21 -1
  18. package/dist/core/extensions/types.d.ts.map +1 -1
  19. package/dist/core/extensions/types.js.map +1 -1
  20. package/dist/core/instruction-resource-loader.d.ts +1 -0
  21. package/dist/core/instruction-resource-loader.d.ts.map +1 -1
  22. package/dist/core/instruction-resource-loader.js +2 -1
  23. package/dist/core/instruction-resource-loader.js.map +1 -1
  24. package/dist/core/messages.d.ts.map +1 -1
  25. package/dist/core/messages.js +30 -1
  26. package/dist/core/messages.js.map +1 -1
  27. package/dist/core/skills.d.ts.map +1 -1
  28. package/dist/core/skills.js +3 -2
  29. package/dist/core/skills.js.map +1 -1
  30. package/dist/core/system-prompt.d.ts +1 -1
  31. package/dist/core/system-prompt.d.ts.map +1 -1
  32. package/dist/core/system-prompt.js +3 -2
  33. package/dist/core/system-prompt.js.map +1 -1
  34. package/dist/core/tools/bash.d.ts +1 -0
  35. package/dist/core/tools/bash.d.ts.map +1 -1
  36. package/dist/core/tools/bash.js +26 -23
  37. package/dist/core/tools/bash.js.map +1 -1
  38. package/dist/core/tools/operations.d.ts +6 -0
  39. package/dist/core/tools/operations.d.ts.map +1 -1
  40. package/dist/core/tools/operations.js +28 -4
  41. package/dist/core/tools/operations.js.map +1 -1
  42. package/dist/index.d.ts +1 -1
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js.map +1 -1
  45. package/dist/main.d.ts.map +1 -1
  46. package/dist/main.js +8 -3
  47. package/dist/main.js.map +1 -1
  48. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  49. package/dist/modes/interactive/components/footer.js +7 -1
  50. package/dist/modes/interactive/components/footer.js.map +1 -1
  51. package/dist/modes/interactive/interactive-mode.d.ts +0 -1
  52. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  53. package/dist/modes/interactive/interactive-mode.js +25 -23
  54. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  55. package/dist/package-manager-cli.d.ts.map +1 -1
  56. package/dist/package-manager-cli.js +33 -2
  57. package/dist/package-manager-cli.js.map +1 -1
  58. package/dist/utils/binary-self-update.d.ts +25 -0
  59. package/dist/utils/binary-self-update.d.ts.map +1 -0
  60. package/dist/utils/binary-self-update.js +159 -0
  61. package/dist/utils/binary-self-update.js.map +1 -0
  62. package/dist/utils/directory-completions.d.ts +8 -0
  63. package/dist/utils/directory-completions.d.ts.map +1 -1
  64. package/dist/utils/directory-completions.js +42 -1
  65. package/dist/utils/directory-completions.js.map +1 -1
  66. package/docs/extensions.md +31 -1
  67. package/docs/sandbox.md +2 -2
  68. package/docs/sessions.md +1 -1
  69. package/docs/skills.md +4 -4
  70. package/docs/usage.md +1 -1
  71. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  72. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  73. package/examples/extensions/sandbox/package.json +1 -1
  74. package/examples/extensions/with-deps/package.json +1 -1
  75. package/npm-shrinkwrap.json +12 -12
  76. package/package.json +4 -4
@@ -15,12 +15,15 @@
15
15
  import { AsyncLocalStorage } from "node:async_hooks";
16
16
  import { randomUUID } from "node:crypto";
17
17
  import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
18
+ import { readFile as fsReadFile } from "node:fs/promises";
18
19
  import { homedir } from "node:os";
19
20
  import { basename, dirname, posix, sep, win32 } from "node:path";
20
21
  import { clampThinkingLevel, cleanupSessionResources, getSupportedThinkingLevels, isContextOverflow, isRetryableAssistantError, modelsAreEqual, resetApiProviders, streamSimple, validateToolArguments, } from "@greppleai/grip-ai";
21
22
  import { Text } from "@greppleai/grip-tui";
22
23
  import { Type } from "typebox";
24
+ import { keyText } from "../modes/interactive/components/keybinding-hints.js";
23
25
  import { theme } from "../modes/interactive/theme/theme.js";
26
+ import { getSandboxDirectoryCompletions } from "../utils/directory-completions.js";
24
27
  import { stripFrontmatter } from "../utils/frontmatter.js";
25
28
  import { canonicalizePath, resolvePath } from "../utils/paths.js";
26
29
  import { sleep } from "../utils/sleep.js";
@@ -32,6 +35,7 @@ import { appendCompressionDetectionEvent, readCompressionDetectionLedger, } from
32
35
  import { CompressionDetector, } from "./compaction/compression-detector.js";
33
36
  import { COMPRESSION_ECONOMICS_ENTRY_TYPE, COMPRESSION_SAVINGS_SNAPSHOT_TYPE, createCompressionSavingsSnapshot, estimateCompressionEconomics, } from "./compaction/compression-economics.js";
34
37
  import { DEFAULT_THINKING_LEVEL } from "./defaults.js";
38
+ import { execCommand } from "./exec.js";
35
39
  import { exportSessionToHtml } from "./export-html/index.js";
36
40
  import { createToolHtmlRenderer } from "./export-html/tool-renderer.js";
37
41
  import { ExtensionRunner, emitSessionShutdownEvent, } from "./extensions/runner.js";
@@ -55,6 +59,7 @@ import { buildSystemPrompt } from "./system-prompt.js";
55
59
  import { createLocalBashOperations } from "./tools/bash.js";
56
60
  import { createCompressContextToolDefinition, getCompressibleEntries, getSuggestedCompressionRangeError, isValidSuggestedCompressionRange, } from "./tools/compress-context.js";
57
61
  import { DeferredRemoteToolOperations, LocalToolOperations, } from "./tools/operations.js";
62
+ import { getTextOutput } from "./tools/render-utils.js";
58
63
  import { createSessionEntryGetToolDefinition, createSessionSearchToolDefinition } from "./tools/session-history.js";
59
64
  import { createCreateSubagentToolDefinition, createSubagentRunsToolDefinition, createSubagentToolDefinition, formatSubagentModelCatalog, } from "./tools/subagent.js";
60
65
  import { createToolDefinitionFromAgentTool } from "./tools/tool-definition-wrapper.js";
@@ -63,7 +68,7 @@ import { WORKSPACE_TOOL_NAMES, WorkspaceToolHost } from "./tools/workspace-tool-
63
68
  // ============================================================================
64
69
  // Skill Block Parsing
65
70
  // ============================================================================
66
- const CORE_DEFAULT_TOOL_NAMES = ["read", "bash", "edit", "write", "websearch"];
71
+ const CORE_DEFAULT_TOOL_NAMES = ["read", "bash", "edit", "write", "websearch", "load_skill"];
67
72
  const SESSION_HISTORY_TOOL_NAMES = ["session_search", "session_entry_get"];
68
73
  export const SUBAGENT_TOOL_NAMES = ["subagent", "subagent_runs", "create_subagent"];
69
74
  export const DEFAULT_ACTIVE_TOOL_NAMES = [...CORE_DEFAULT_TOOL_NAMES, ...SESSION_HISTORY_TOOL_NAMES];
@@ -388,6 +393,11 @@ function isWithinSandboxMount(directory, root, flavor) {
388
393
  const relativePath = paths.relative(root, directory);
389
394
  return relativePath !== ".." && !relativePath.startsWith(`..${paths.sep}`) && !paths.isAbsolute(relativePath);
390
395
  }
396
+ function hasValidHostProcessApproval(options) {
397
+ return (typeof options?.question === "string" &&
398
+ !!options.question.trim() &&
399
+ (options.explanation === undefined || (typeof options.explanation === "string" && !!options.explanation.trim())));
400
+ }
391
401
  export class AgentSession {
392
402
  constructor(config) {
393
403
  this._eventListeners = [];
@@ -422,6 +432,7 @@ export class AgentSession {
422
432
  this._activities = new Set();
423
433
  this._activityGeneration = 0;
424
434
  this._activeCycle = false;
435
+ this._hostCommandApprovalQueue = Promise.resolve();
425
436
  this._settlementScheduled = false;
426
437
  this._settlementRunning = false;
427
438
  this._idleWaiters = new Set();
@@ -598,6 +609,11 @@ export class AgentSession {
598
609
  getToolBackendInfo() {
599
610
  return this.getToolOperations().getBackendInfo?.() ?? { type: "local", cwd: this._cwd };
600
611
  }
612
+ /** Whether the current session is using a live, session-owned sandbox connection. */
613
+ isSandbox() {
614
+ const operations = this._sandboxToolOperations;
615
+ return !!operations && operations === this._toolOperations && !operations.isDisconnected();
616
+ }
601
617
  supportsDirectoryChange() {
602
618
  return this._toolOperations === undefined && this.getToolBackendInfo().type === "local";
603
619
  }
@@ -668,7 +684,19 @@ export class AgentSession {
668
684
  }));
669
685
  }
670
686
  async activateSandboxDaemon(options) {
671
- return this._runActivity("sandbox daemon activation", () => this._enqueueSandboxTransition(() => this._activateSessionRemoteOperations(options.expectedCwd, (operations) => operations.configureRemote(options.url, { token: options.token, expectedCwd: options.expectedCwd }))));
687
+ return this._runActivity("sandbox daemon activation", () => this._enqueueSandboxTransition(() => this._activateSessionRemoteOperations(options.expectedCwd, (operations) => operations.configureRemote(options.url, {
688
+ token: options.token,
689
+ expectedCwd: options.expectedCwd,
690
+ hostWorkspaceRoot: options.hostWorkspaceRoot,
691
+ }))));
692
+ }
693
+ /** Complete only directories available inside the connected sandbox's mounted workspace. */
694
+ async getSandboxDirectoryCompletions(prefix) {
695
+ const operations = this._sandboxToolOperations;
696
+ const backend = operations?.getBackendInfo();
697
+ if (!operations || operations !== this._toolOperations || backend?.type !== "remote" || !backend.configured)
698
+ return [];
699
+ return getSandboxDirectoryCompletions(prefix, operations.cwd, backend.workspace.pathFlavor, operations);
672
700
  }
673
701
  /** Change only the connected sandbox's working directory, never the host session or mount. */
674
702
  async changeSandboxWorkingDirectory(target) {
@@ -4069,6 +4097,7 @@ export class AgentSession {
4069
4097
  getSystemPrompt: () => this.systemPrompt,
4070
4098
  getToolOperations,
4071
4099
  getToolBackendInfo: () => this.getToolBackendInfo(),
4100
+ isSandbox: () => this.isSandbox(),
4072
4101
  getLspStatus: getBoundLspStatus,
4073
4102
  configureLsp: (configuration) => {
4074
4103
  if (runner !== this._extensionRunner)
@@ -4081,6 +4110,20 @@ export class AgentSession {
4081
4110
  const resolvedCommand = prefix ? `${prefix}\n${command}` : command;
4082
4111
  return executeBashWithOperations(resolvedCommand, options?.cwd ?? operations.cwd, operations, options);
4083
4112
  }),
4113
+ execHostCommand: (command, options) => {
4114
+ const execute = () => this._runActivity("extension host command approval", () => this._executeApprovedHostCommand(runner, command, options));
4115
+ const pending = this._hostCommandApprovalQueue.then(execute, execute);
4116
+ this._hostCommandApprovalQueue = pending.then(() => undefined, () => undefined);
4117
+ return pending;
4118
+ },
4119
+ execHostProcess: (executable, args, options) => {
4120
+ const argv = Array.isArray(args) ? [...args] : args;
4121
+ const request = { ...options };
4122
+ const execute = () => this._runActivity("extension host process approval", () => this._executeApprovedHostProcess(runner, executable, argv, request));
4123
+ const pending = this._hostCommandApprovalQueue.then(execute, execute);
4124
+ this._hostCommandApprovalQueue = pending.then(() => undefined, () => undefined);
4125
+ return pending;
4126
+ },
4084
4127
  }, {
4085
4128
  registerProvider: (name, config) => {
4086
4129
  this._modelRegistry.registerProvider(name, config);
@@ -4092,6 +4135,132 @@ export class AgentSession {
4092
4135
  },
4093
4136
  });
4094
4137
  }
4138
+ async _executeApprovedHostCommand(runner, command, options) {
4139
+ if (runner !== this._extensionRunner)
4140
+ throw new Error(STALE_EXTENSION_CONTEXT_MESSAGE);
4141
+ if (typeof command !== "string" ||
4142
+ !command.trim() ||
4143
+ typeof options?.question !== "string" ||
4144
+ !options.question.trim()) {
4145
+ throw new Error("Host command and approval question must be non-empty strings");
4146
+ }
4147
+ const operations = this._sandboxToolOperations;
4148
+ if (!operations || operations !== this._toolOperations || operations.isDisconnected()) {
4149
+ throw new Error("A connected sandbox is required for approved host commands");
4150
+ }
4151
+ if (!runner.hasUI())
4152
+ throw new Error("Host command requires an interactive approval UI");
4153
+ const signal = options.signal ?? this.agent.signal;
4154
+ if (signal?.aborted)
4155
+ throw new Error("Host command cancelled");
4156
+ const hostCwd = await operations.mapWorkspaceDirectoryToHost(options.cwd ?? operations.cwd);
4157
+ const choice = await runner
4158
+ .getUIContext()
4159
+ .select(`Run on host?\nReason: ${JSON.stringify(options.question)}\nCommand: ${JSON.stringify(command)}\nHost cwd: ${JSON.stringify(hostCwd)}`, ["No", "Yes"], { signal });
4160
+ if (choice !== "Yes" || signal?.aborted)
4161
+ throw new Error("Host command was not approved");
4162
+ if (runner !== this._extensionRunner ||
4163
+ operations !== this._sandboxToolOperations ||
4164
+ operations.isDisconnected()) {
4165
+ throw new Error("Sandbox changed before the host command could run");
4166
+ }
4167
+ return executeBashWithOperations(command, hostCwd, new LocalToolOperations(hostCwd), { ...options, signal });
4168
+ }
4169
+ async _executeApprovedHostProcess(runner, executable, args, options) {
4170
+ if (runner !== this._extensionRunner)
4171
+ throw new Error(STALE_EXTENSION_CONTEXT_MESSAGE);
4172
+ if (typeof executable !== "string" ||
4173
+ !executable.trim() ||
4174
+ executable.includes("\0") ||
4175
+ !Array.isArray(args) ||
4176
+ args.some((arg) => typeof arg !== "string" || arg.includes("\0")) ||
4177
+ !hasValidHostProcessApproval(options)) {
4178
+ throw new Error("Host executable, argv and approval question must be valid strings");
4179
+ }
4180
+ const operations = this._sandboxToolOperations;
4181
+ if (!operations || operations !== this._toolOperations || operations.isDisconnected()) {
4182
+ throw new Error("A connected sandbox is required for approved host processes");
4183
+ }
4184
+ if (!runner.hasUI())
4185
+ throw new Error("Host process requires an interactive approval UI");
4186
+ const signal = options.signal ?? this.agent.signal;
4187
+ if (signal?.aborted)
4188
+ throw new Error("Host process cancelled");
4189
+ const hostCwd = await operations.mapWorkspaceDirectoryToHost(options.cwd ?? operations.cwd);
4190
+ const explanation = options.explanation === undefined ? "" : `\nExplanation (unverified): ${JSON.stringify(options.explanation)}`;
4191
+ const choice = await runner
4192
+ .getUIContext()
4193
+ .select(`Run on host (no shell)?\nReason: ${JSON.stringify(options.question)}${explanation}\nExecutable: ${JSON.stringify(executable)}\nArguments: ${JSON.stringify(args)}\nHost cwd: ${JSON.stringify(hostCwd)}`, ["No", "Yes"], { signal });
4194
+ if (choice !== "Yes" || signal?.aborted)
4195
+ throw new Error("Host process was not approved");
4196
+ if (runner !== this._extensionRunner ||
4197
+ operations !== this._sandboxToolOperations ||
4198
+ operations.isDisconnected()) {
4199
+ throw new Error("Sandbox changed before the host process could run");
4200
+ }
4201
+ return execCommand(executable, args, hostCwd, { signal, timeout: options.timeout });
4202
+ }
4203
+ _createLoadSkillToolDefinition() {
4204
+ const parameters = Type.Object({
4205
+ name: Type.Union([Type.String(), Type.Array(Type.String())], {
4206
+ description: "Name of a listed skill, or an array of skill names to load in one call",
4207
+ }),
4208
+ });
4209
+ return {
4210
+ name: "load_skill",
4211
+ label: "Load skill",
4212
+ description: "Load the full instructions for one or more available skills by name. Use the reported skill directory to resolve relative references. Hidden skills and unknown names are not available.",
4213
+ promptSnippet: "Load instructions for available skills by name (or an array of names)",
4214
+ parameters,
4215
+ renderCall: (args, renderTheme, context) => {
4216
+ const names = getToolNameArgs(args);
4217
+ const expandKey = keyText("app.tools.expand");
4218
+ const hint = !context.expanded && expandKey ? renderTheme.fg("dim", ` (${expandKey} to expand)`) : "";
4219
+ return new Text(renderTheme.fg("customMessageLabel", `\x1b[1m[skill]\x1b[22m `) +
4220
+ renderTheme.fg("customMessageText", names.join(", ")) +
4221
+ hint, 0, 0);
4222
+ },
4223
+ renderResult: (result, options, renderTheme, context) => {
4224
+ const output = options.expanded || context.isError ? getTextOutput(result, context.showImages) : "";
4225
+ return new Text(output ? `\n${renderTheme.fg(context.isError ? "error" : "toolOutput", output)}` : "", 0, 0);
4226
+ },
4227
+ executionMode: "sequential",
4228
+ execute: async (_toolCallId, params, signal) => {
4229
+ if (signal?.aborted)
4230
+ throw new Error("Skill load cancelled");
4231
+ const names = [...new Set(Array.isArray(params.name) ? params.name : [params.name])];
4232
+ if (names.length === 0 || names.some((name) => !name))
4233
+ throw new Error("Specify at least one skill name");
4234
+ const availableSkills = new Map(this.getRegisteredSkills()
4235
+ .filter((skill) => !skill.disableModelInvocation)
4236
+ .map((skill) => [skill.name, skill]));
4237
+ const missing = names.filter((name) => !availableSkills.has(name));
4238
+ if (missing.length > 0)
4239
+ throw new Error(`Skill not available: ${missing.join(", ")}`);
4240
+ const skills = names.map((name) => availableSkills.get(name));
4241
+ const content = await Promise.all(skills.map(async (skill) => {
4242
+ let text = skill.content;
4243
+ if (text === undefined && getSourceBackend(skill.sourceInfo) === "remote") {
4244
+ if (this.getToolBackendInfo().type !== "remote")
4245
+ throw new Error(`Remote skill not available: ${skill.name}`);
4246
+ text = (await this.getToolOperations().readFile(skill.filePath)).toString("utf-8");
4247
+ }
4248
+ else if (text === undefined) {
4249
+ text = await fsReadFile(resolvePath(skill.filePath, this._cwd), "utf-8");
4250
+ }
4251
+ return {
4252
+ type: "text",
4253
+ text: `Skill: ${JSON.stringify(skill.name)}\nSkill directory: ${JSON.stringify(skill.baseDir)}\nFile: ${JSON.stringify(skill.filePath)}\n\n${text}`,
4254
+ };
4255
+ }));
4256
+ if (signal?.aborted)
4257
+ throw new Error("Skill load cancelled");
4258
+ for (const skill of skills)
4259
+ this._loadAssociatedInstructionTools(skill);
4260
+ return { content, details: { names } };
4261
+ },
4262
+ };
4263
+ }
4095
4264
  _createLoadToolDefinition() {
4096
4265
  const parameters = Type.Object({
4097
4266
  name: Type.Union([Type.String(), Type.Array(Type.String())], {
@@ -4442,6 +4611,7 @@ export class AgentSession {
4442
4611
  const baseToolDefinitions = this._createRuntimeBaseToolDefinitions(workspaceToolHost);
4443
4612
  this._baseToolDefinitions = new Map(Object.entries(baseToolDefinitions));
4444
4613
  this._initializeRuntimeLsp(operations);
4614
+ this._baseToolDefinitions.set("load_skill", this._createLoadSkillToolDefinition());
4445
4615
  this._baseToolDefinitions.set("load_tool", this._createLoadToolDefinition());
4446
4616
  this._baseToolDefinitions.set("unload_tool", this._createUnloadToolDefinition());
4447
4617
  const extensionsResult = this._resourceLoader.getExtensions();