Compare commits

..

2 Commits

Author SHA1 Message Date
Sehoon Shon 2504a264db fix(cli): add folder trust check for /note and fix subcommand collision 2026-04-21 16:56:01 -07:00
Sehoon Shon ad98bedc6e feat(cli): add /note command to append and view workspace notes 2026-04-21 16:47:20 -07:00
4 changed files with 205 additions and 132 deletions
+53 -32
View File
@@ -449,8 +449,7 @@
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.11.0.tgz",
"integrity": "sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ==",
"license": "(Apache-2.0 AND BSD-3-Clause)",
"peer": true
"license": "(Apache-2.0 AND BSD-3-Clause)"
},
"node_modules/@bundled-es-modules/cookie": {
"version": "2.0.1",
@@ -1474,7 +1473,6 @@
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.4.tgz",
"integrity": "sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@grpc/proto-loader": "^0.7.13",
"@js-sdsl/ordered-map": "^4.4.2"
@@ -2152,7 +2150,6 @@
"integrity": "sha512-t54CUOsFMappY1Jbzb7fetWeO0n6K0k/4+/ZpkS+3Joz8I4VcvY9OiEBFRYISqaI2fq5sCiPtAjRDOzVYG8m+Q==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@octokit/auth-token": "^6.0.0",
"@octokit/graphql": "^9.0.2",
@@ -2333,7 +2330,6 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">=8.0.0"
}
@@ -2383,7 +2379,6 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz",
"integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@opentelemetry/semantic-conventions": "^1.29.0"
},
@@ -2758,7 +2753,6 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.5.0.tgz",
"integrity": "sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@opentelemetry/core": "2.5.0",
"@opentelemetry/semantic-conventions": "^1.29.0"
@@ -2792,7 +2786,6 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.5.0.tgz",
"integrity": "sha512-BeJLtU+f5Gf905cJX9vXFQorAr6TAfK3SPvTFqP+scfIpDQEJfRaGJWta7sJgP+m4dNtBf9y3yvBKVAZZtJQVA==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@opentelemetry/core": "2.5.0",
"@opentelemetry/resources": "2.5.0"
@@ -2847,7 +2840,6 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.5.0.tgz",
"integrity": "sha512-VzRf8LzotASEyNDUxTdaJ9IRJ1/h692WyArDBInf5puLCjxbICD6XkHgpuudis56EndyS7LYFmtTMny6UABNdQ==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@opentelemetry/core": "2.5.0",
"@opentelemetry/resources": "2.5.0",
@@ -4054,7 +4046,6 @@
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
@@ -4328,7 +4319,6 @@
"integrity": "sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.58.2",
"@typescript-eslint/types": "8.58.2",
@@ -4603,6 +4593,56 @@
}
}
},
"node_modules/@vitest/eslint-plugin/node_modules/@typescript-eslint/scope-manager": {
"version": "8.47.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.47.0.tgz",
"integrity": "sha512-a0TTJk4HXMkfpFkL9/WaGTNuv7JWfFTQFJd6zS9dVAjKsojmv9HT55xzbEpnZoY+VUb+YXLMp+ihMLz/UlZfDg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.47.0",
"@typescript-eslint/visitor-keys": "8.47.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@vitest/eslint-plugin/node_modules/@typescript-eslint/types": {
"version": "8.47.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.47.0.tgz",
"integrity": "sha512-nHAE6bMKsizhA2uuYZbEbmp5z2UpffNrPEqiKIeN7VsV6UY/roxanWfoRrf6x/k9+Obf+GQdkm0nPU+vnMXo9A==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@vitest/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": {
"version": "8.47.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.47.0.tgz",
"integrity": "sha512-SIV3/6eftCy1bNzCQoPmbWsRLujS8t5iDIZ4spZOBHqrM+yfX2ogg8Tt3PDTAVKw3sSCiUgg30uOAvK2r9zGjQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.47.0",
"eslint-visitor-keys": "^4.2.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@vitest/expect": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz",
@@ -5073,7 +5113,6 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -7151,8 +7190,7 @@
"version": "0.0.1581282",
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1581282.tgz",
"integrity": "sha512-nv7iKtNZQshSW2hKzYNr46nM/Cfh5SEvE2oV0/SEGgc9XupIY5ggf84Cz8eJIkBce7S3bmTAauFD6aysMpnqsQ==",
"license": "BSD-3-Clause",
"peer": true
"license": "BSD-3-Clause"
},
"node_modules/dezalgo": {
"version": "1.0.4",
@@ -7737,7 +7775,6 @@
"integrity": "sha512-GsGizj2Y1rCWDu6XoEekL3RLilp0voSePurjZIkxL3wlm5o5EC9VpgaP7lrCvjnkuLvzFBQWB3vWB3K5KQTveQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -8255,7 +8292,6 @@
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
"license": "MIT",
"peer": true,
"dependencies": {
"accepts": "^2.0.0",
"body-parser": "^2.2.1",
@@ -9522,7 +9558,6 @@
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.12.tgz",
"integrity": "sha512-p1JfQMKaceuCbpJKAPKVqyqviZdS0eUxH9v82oWo1kb9xjQ5wA6iP3FNVAPDFlz5/p7d45lO+BpSk1tuSZMF4Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=16.9.0"
}
@@ -9782,7 +9817,6 @@
"resolved": "https://registry.npmjs.org/@jrichman/ink/-/ink-6.6.9.tgz",
"integrity": "sha512-RL9sSiLQZECnjbmBwjIHOp8yVGdWF7C/uifg7ISv/e+F3nLNsfl7FdUFQs8iZARFMJAYxMFpxW6OW+HSt9drwQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"ansi-escapes": "^7.0.0",
"ansi-styles": "^6.2.3",
@@ -13496,7 +13530,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -13507,7 +13540,6 @@
"integrity": "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"shell-quote": "^1.6.1",
"ws": "^7"
@@ -15627,7 +15659,6 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -15850,8 +15881,7 @@
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD",
"peer": true
"license": "0BSD"
},
"node_modules/tsx": {
"version": "4.20.3",
@@ -15859,7 +15889,6 @@
"integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "~0.25.0",
"get-tsconfig": "^4.7.5"
@@ -16025,7 +16054,6 @@
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -16093,7 +16121,6 @@
"integrity": "sha512-6sMvZePQrnZH2/cJkwRpkT7DxoAWh+g6+GFRK6bV3YQo7ogi3SX5rgF6099r5Q53Ma5qeT7LGmOmuIutF4t3lA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.35.0",
"@typescript-eslint/types": "8.35.0",
@@ -16480,7 +16507,6 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz",
"integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==",
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.27.0",
"fdir": "^6.5.0",
@@ -17051,7 +17077,6 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -17064,7 +17089,6 @@
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz",
"integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/chai": "^5.2.2",
"@vitest/expect": "3.2.4",
@@ -17703,7 +17727,6 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
@@ -18139,7 +18162,6 @@
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz",
"integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@grpc/proto-loader": "^0.8.0",
"@js-sdsl/ordered-map": "^4.4.2"
@@ -18258,7 +18280,6 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -185,8 +185,9 @@ export class BuiltinCommandLoader implements ICommandLoader {
: [mcpCommand]),
memoryCommand,
modelCommand,
noteCommand,
...(this.config?.getFolderTrust() ? [permissionsCommand] : []),
...(this.config?.getFolderTrust()
? [permissionsCommand, noteCommand]
: []),
...(this.config?.isPlanEnabled() ? [planCommand] : []),
policiesCommand,
privacyCommand,
@@ -1,94 +1,115 @@
/**
* @license
* Copyright 2026 Google LLC
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach } from 'vitest';
import fs from 'node:fs/promises';
import path from 'node:path';
import { vi, describe, it, expect, beforeEach } from 'vitest';
import { noteCommand } from './noteCommand.js';
import { type CommandContext } from './types.js';
import * as fsPromises from 'node:fs/promises';
import { createMockCommandContext } from '../../test-utils/mockCommandContext.js';
import * as path from 'node:path';
import type { MessageActionReturn } from '@google/gemini-cli-core';
vi.mock('node:fs/promises');
describe('noteCommand', () => {
const mockContext = {} as CommandContext;
const notesPath = path.join(process.cwd(), 'notes.md');
const mockContext = createMockCommandContext();
const workspaceRoot = process.cwd();
const notesFile = path.join(workspaceRoot, 'notes.md');
beforeEach(() => {
vi.clearAllMocks();
});
it('should return notes content when no args provided and file exists', async () => {
vi.mocked(fs.readFile).mockResolvedValue('existing note\n');
describe('append action', () => {
it('should append a note to notes.md when args are provided', async () => {
const noteText = 'This is a test note';
vi.mocked(fsPromises.appendFile).mockResolvedValue(undefined);
const result = await noteCommand.action!(mockContext, '');
const result = (await noteCommand.action!(
mockContext,
noteText,
)) as MessageActionReturn;
expect(fs.readFile).toHaveBeenCalledWith(notesPath, 'utf8');
expect(result).toEqual({
type: 'message',
messageType: 'info',
content: expect.stringContaining('existing note'),
expect(fsPromises.appendFile).toHaveBeenCalledWith(
notesFile,
expect.stringContaining(noteText),
);
expect(fsPromises.appendFile).toHaveBeenCalledWith(
notesFile,
expect.stringContaining('## '),
);
expect(result).toEqual({
type: 'message',
messageType: 'info',
content: `Note saved to ${notesFile}`,
});
});
it('should return a usage message when no args are provided', async () => {
const result = (await noteCommand.action!(
mockContext,
' ',
)) as MessageActionReturn;
expect(fsPromises.appendFile).not.toHaveBeenCalled();
expect(result.content).toContain('Please provide a note to save');
});
it('should return an error message when appendFile fails', async () => {
vi.mocked(fsPromises.appendFile).mockRejectedValue(new Error('FS Error'));
const result = (await noteCommand.action!(
mockContext,
'some note',
)) as MessageActionReturn;
expect(result.content).toContain('Failed to save note: FS Error');
});
});
it('should return info message when no args provided and file does not exist (ENOENT)', async () => {
const error = new Error('File not found') as NodeJS.ErrnoException;
error.code = 'ENOENT';
vi.mocked(fs.readFile).mockRejectedValue(error);
const result = await noteCommand.action!(mockContext, ' ');
expect(result).toEqual({
type: 'message',
messageType: 'info',
content: 'No notes found. Use "/note <text>" to add one.',
});
});
it('should return error message when readFile fails with other error', async () => {
vi.mocked(fs.readFile).mockRejectedValue(new Error('Permission denied'));
const result = await noteCommand.action!(mockContext, '');
expect(result).toEqual({
type: 'message',
messageType: 'error',
content: expect.stringContaining(
'Failed to read notes: Permission denied',
),
});
});
it('should append trimmed note to file when args are provided', async () => {
const note = ' this is a new note ';
vi.mocked(fs.appendFile).mockResolvedValue(undefined);
const result = await noteCommand.action!(mockContext, note);
expect(fs.appendFile).toHaveBeenCalledWith(
notesPath,
`this is a new note\n`,
describe('view subcommand', () => {
const viewSubcommand = noteCommand.subCommands?.find(
(s) => s.name === 'view',
);
expect(result).toEqual({
type: 'message',
messageType: 'info',
content: expect.stringContaining('Note added'),
it('should read and display notes from notes.md', async () => {
const notesContent = '## 4/21/2026\n\nTest note content';
vi.mocked(fsPromises.readFile).mockResolvedValue(notesContent);
const result = (await viewSubcommand!.action!(
mockContext,
'',
)) as MessageActionReturn;
expect(fsPromises.readFile).toHaveBeenCalledWith(notesFile, 'utf8');
expect(result.content).toContain('### Current Notes');
expect(result.content).toContain(notesContent);
});
});
it('should return error message when append fails', async () => {
vi.mocked(fs.appendFile).mockRejectedValue(new Error('Permission denied'));
it('should return "No notes found" if notes.md does not exist', async () => {
const error = new Error('Not found');
Object.assign(error, { code: 'ENOENT' });
vi.mocked(fsPromises.readFile).mockRejectedValue(error);
const result = await noteCommand.action!(mockContext, 'some note');
const result = (await viewSubcommand!.action!(
mockContext,
'',
)) as MessageActionReturn;
expect(result).toEqual({
type: 'message',
messageType: 'error',
content: expect.stringContaining(
'Failed to save note: Permission denied',
),
expect(result.content).toContain('No notes found in this workspace.');
});
it('should return an error message when readFile fails with other error', async () => {
vi.mocked(fsPromises.readFile).mockRejectedValue(new Error('Read Error'));
const result = (await viewSubcommand!.action!(
mockContext,
'',
)) as MessageActionReturn;
expect(result.content).toContain('Failed to read notes: Read Error');
});
});
});
+62 -32
View File
@@ -1,59 +1,89 @@
/**
* @license
* Copyright 2026 Google LLC
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import fs from 'node:fs/promises';
import path from 'node:path';
import { isNodeError } from '@google/gemini-cli-core';
import { CommandKind, type SlashCommand } from './types.js';
import * as fsPromises from 'node:fs/promises';
import * as path from 'node:path';
export const noteCommand: SlashCommand = {
name: 'note',
description: 'Append a note to notes.md or view current notes',
description: 'Manage workspace notes (append or view)',
kind: CommandKind.BUILT_IN,
autoExecute: true,
action: async (_context, args) => {
const notesPath = path.join(process.cwd(), 'notes.md');
subCommands: [
{
name: 'view',
description: 'View the current workspace notes',
kind: CommandKind.BUILT_IN,
autoExecute: true,
takesArgs: false,
action: async () => {
const workspaceRoot = process.cwd();
const notesFile = path.join(workspaceRoot, 'notes.md');
if (!args || args.trim().length === 0) {
try {
const content = await fs.readFile(notesPath, 'utf8');
return {
type: 'message',
messageType: 'info',
content: `Current notes in ${notesPath}:\n\n${content}`,
};
} catch (error) {
if (isNodeError(error) && error.code === 'ENOENT') {
try {
const content = await fsPromises.readFile(notesFile, 'utf8');
return {
type: 'message',
messageType: 'info',
content: 'No notes found. Use "/note <text>" to add one.',
content: `### Current Notes\n\n${content}`,
};
} catch (err: unknown) {
if (
err &&
typeof err === 'object' &&
'code' in err &&
err.code === 'ENOENT'
) {
return {
type: 'message',
messageType: 'info',
content: 'No notes found in this workspace.',
};
}
const errorMessage = err instanceof Error ? err.message : String(err);
return {
type: 'message',
messageType: 'error',
content: `Failed to read notes: ${errorMessage}`,
};
}
return {
type: 'message',
messageType: 'error',
content: `Failed to read notes: ${error instanceof Error ? error.message : String(error)}`,
};
}
}
try {
const trimmedNote = args.trim();
await fs.appendFile(notesPath, `${trimmedNote}\n`);
},
},
],
action: async (context, args) => {
if (!args.trim()) {
return {
type: 'message',
messageType: 'info',
content: `Note added to ${notesPath}`,
content:
'Please provide a note to save or use `/note view` to see your notes. Example: `/note This is a useful thought.`',
};
} catch (error) {
}
const workspaceRoot = process.cwd();
const notesFile = path.join(workspaceRoot, 'notes.md');
const timestamp = new Date().toLocaleString();
const noteEntry = `\n## ${timestamp}\n\n${args.trim()}\n`;
try {
await fsPromises.appendFile(notesFile, noteEntry);
return {
type: 'message',
messageType: 'info',
content: `Note saved to ${notesFile}`,
};
} catch (err: unknown) {
const errorMessage = err instanceof Error ? err.message : String(err);
return {
type: 'message',
messageType: 'error',
content: `Failed to save note: ${error instanceof Error ? error.message : String(error)}`,
content: `Failed to save note: ${errorMessage}`,
};
}
},