···33 * Note: groups commands are now provided by the groups extension
44 * Note: sync command is now provided by the sync extension
55 * Note: open/modal commands are now provided by the page extension
66+ * Note: tag, tags, untag, tagset commands are now provided by the tags extension
67 */
78import debugCommand from './debug.js';
89import noteModule from './note.js';
99-import tagsetModule from './tagset.js';
1010import urlModule from './url.js';
1111import historyModule from './history.js';
1212-import tagModule from './tag.js';
13121413// Chaining commands - for command composition pipelines
1514import listsCommand from './lists.js';
1615// csv and save commands moved to files extension
17161818-console.log('[cmd:commands/index] tagModule.commands:', tagModule.commands?.map(c => c.name));
1919-2017// Source commands (commented out as they need browser extension APIs)
2118// These modules contain command sources that dynamically generate commands
2219import bookmarkletsSource from './bookmarklets.js';
···3330// Note: groups commands are dynamically registered by the groups extension
3431// Note: sync command is dynamically registered by the sync extension
3532// Note: open/modal commands are dynamically registered by the page extension
3333+// Note: tag, tags, untag, tagset commands are dynamically registered by the tags extension
3634const activeCommands = [
3735 debugCommand,
3836 ...noteModule.commands,
3939- ...tagsetModule.commands,
4037 ...urlModule.commands,
4138 ...historyModule.commands,
4242- ...tagModule.commands,
43394440 // Chaining commands
4541 listsCommand
-287
extensions/cmd/commands/tag.js
···11-/**
22- * Tag command - add tags to the URL of the active window
33- * Tags are saved using the items table with item_tags join table
44- *
55- * Usage:
66- * tag foo - add tag "foo" to active window's URL
77- * tag foo bar - add multiple tags
88- * tag -r foo - remove tag "foo" from active window
99- * tag - show tags for active window
1010- */
1111-import api from 'peek://app/api.js';
1212-1313-/**
1414- * Get the most recently focused non-internal window
1515- */
1616-const getActiveWindow = async () => {
1717- const result = await api.window.list({ includeInternal: false });
1818- if (!result.success || !result.windows.length) {
1919- return null;
2020- }
2121- // Return the first non-internal window
2222- return result.windows[0];
2323-};
2424-2525-/**
2626- * Find item record by URL content
2727- */
2828-const findItemByUrl = async (url) => {
2929- const result = await api.datastore.queryItems({ type: 'url' });
3030- if (!result.success) return null;
3131-3232- return result.data.find(item => item.content === url) || null;
3333-};
3434-3535-/**
3636- * Add tags to an item using the join table
3737- */
3838-const addTagsToItem = async (itemId, tagNames) => {
3939- const results = [];
4040-4141- for (const tagName of tagNames) {
4242- // Get or create the tag
4343- api.log('[tag] Getting/creating tag:', tagName);
4444- const tagResult = await api.datastore.getOrCreateTag(tagName);
4545- api.log('[tag] getOrCreateTag result:', JSON.stringify(tagResult));
4646- if (!tagResult.success) {
4747- console.error('Failed to get/create tag:', tagName, tagResult.error);
4848- continue;
4949- }
5050-5151- const tag = tagResult.data.tag;
5252- api.log('[tag] Tag id:', tag.id, 'name:', tag.name);
5353-5454- // Link tag to item
5555- api.log('[tag] Linking tag', tag.id, 'to item', itemId);
5656- const linkResult = await api.datastore.tagItem(itemId, tag.id);
5757- api.log('[tag] tagItem result:', JSON.stringify(linkResult));
5858- if (!linkResult.success) {
5959- console.error('Failed to link tag:', tagName, linkResult.error);
6060- continue;
6161- }
6262-6363- results.push({
6464- tag,
6565- alreadyExists: linkResult.alreadyExists
6666- });
6767- }
6868-6969- return results;
7070-};
7171-7272-/**
7373- * Remove tags from an item
7474- */
7575-const removeTagsFromItem = async (itemId, tagNames) => {
7676- const results = [];
7777-7878- // Get current tags for item
7979- const tagsResult = await api.datastore.getItemTags(itemId);
8080- if (!tagsResult.success) {
8181- return results;
8282- }
8383-8484- for (const tagName of tagNames) {
8585- // Find the tag by name
8686- const tag = tagsResult.data.find(t => t.name.toLowerCase() === tagName.toLowerCase());
8787- if (!tag) {
8888- console.log('Tag not found on item:', tagName);
8989- continue;
9090- }
9191-9292- // Unlink tag from item
9393- const unlinkResult = await api.datastore.untagItem(itemId, tag.id);
9494- results.push({
9595- tag,
9696- removed: unlinkResult.success
9797- });
9898- }
9999-100100- return results;
101101-};
102102-103103-/**
104104- * Get tags for an item
105105- */
106106-const getTagsForItem = async (itemId) => {
107107- const result = await api.datastore.getItemTags(itemId);
108108- if (!result.success) return [];
109109- return result.data;
110110-};
111111-112112-// Commands
113113-const commands = [
114114- {
115115- name: 'tag',
116116- description: 'Add tags to the active window URL',
117117- async execute(ctx) {
118118- // Get active window
119119- api.log('tag command execute, ctx:', ctx);
120120- const activeWindow = await getActiveWindow();
121121- api.log('tag command: activeWindow =', activeWindow);
122122- if (!activeWindow) {
123123- api.log('No active window found');
124124- return { success: false, error: 'No active window' };
125125- }
126126-127127- const url = activeWindow.url;
128128- api.log('Tagging URL:', url);
129129-130130- // Find item in datastore
131131- let item = await findItemByUrl(url);
132132-133133- // If no item exists, create one
134134- if (!item) {
135135- api.log('[tag] Creating new item for URL:', url);
136136- const addResult = await api.datastore.addItem('url', {
137137- content: url,
138138- metadata: JSON.stringify({ title: activeWindow.title || '' })
139139- });
140140- api.log('[tag] addItem result:', JSON.stringify(addResult));
141141- if (!addResult.success) {
142142- console.error('Failed to create item:', addResult.error);
143143- return { success: false, error: 'Failed to create item' };
144144- }
145145- item = { id: addResult.data.id };
146146- api.log('[tag] Created item with id:', item.id);
147147- } else {
148148- api.log('[tag] Found existing item:', item.id, 'for URL:', url);
149149- }
150150-151151- // No args - show current tags
152152- if (!ctx.search) {
153153- const tags = await getTagsForItem(item.id);
154154- if (tags.length === 0) {
155155- console.log('No tags for:', url);
156156- } else {
157157- console.log('Tags for', url + ':');
158158- tags.forEach(t => console.log(' -', t.name, `(frecency: ${t.frecencyScore?.toFixed(1) || 0})`));
159159- }
160160- return { success: true, tags };
161161- }
162162-163163- // Parse args
164164- // If comma present, split on comma; otherwise split on spaces
165165- const input = ctx.search.trim();
166166- const hasComma = input.includes(',');
167167- let args;
168168- if (hasComma) {
169169- args = input.split(',').map(s => s.trim()).filter(s => s.length > 0);
170170- } else {
171171- args = input.split(/\s+/);
172172- }
173173- const removeMode = args[0] === '-r';
174174- const tagsToProcess = removeMode ? args.slice(1) : args;
175175-176176- if (tagsToProcess.length === 0) {
177177- console.log('No tags specified');
178178- return { success: false, error: 'No tags specified' };
179179- }
180180-181181- // Add or remove tags
182182- if (removeMode) {
183183- const results = await removeTagsFromItem(item.id, tagsToProcess);
184184- const removed = results.filter(r => r.removed).map(r => r.tag.name);
185185- if (removed.length > 0) {
186186- console.log('Removed tags:', removed.join(', '), 'from', url);
187187- }
188188- return { success: true, removed };
189189- } else {
190190- api.log('Adding tags to item:', item.id, 'tags:', tagsToProcess);
191191- const results = await addTagsToItem(item.id, tagsToProcess);
192192- api.log('addTagsToItem results:', results);
193193- const added = results.filter(r => !r.alreadyExists).map(r => r.tag.name);
194194- const existing = results.filter(r => r.alreadyExists).map(r => r.tag.name);
195195- if (added.length > 0) {
196196- console.log('Added tags:', added.join(', '), 'to', url);
197197- }
198198- if (existing.length > 0) {
199199- console.log('Already tagged:', existing.join(', '));
200200- }
201201- return { success: true, added, existing };
202202- }
203203- }
204204- },
205205- {
206206- name: 'tags',
207207- description: 'Show tags for the active window URL (or all tags by frecency)',
208208- async execute(ctx) {
209209- // If search term provided, show all tags matching
210210- if (ctx.search) {
211211- const result = await api.datastore.getTagsByFrecency();
212212- if (!result.success) {
213213- console.log('Failed to get tags');
214214- return { success: false };
215215- }
216216-217217- const filter = ctx.search.toLowerCase();
218218- const filtered = result.data.filter(t => t.name.toLowerCase().includes(filter));
219219-220220- if (filtered.length === 0) {
221221- console.log('No tags matching:', ctx.search);
222222- } else {
223223- console.log('Tags matching "' + ctx.search + '":');
224224- filtered.forEach(t => {
225225- console.log(' -', t.name, `(used ${t.frequency}x, frecency: ${t.frecencyScore?.toFixed(1) || 0})`);
226226- });
227227- }
228228- return { success: true, tags: filtered };
229229- }
230230-231231- // No args - show tags for active window
232232- const activeWindow = await getActiveWindow();
233233- if (!activeWindow) {
234234- // No active window - show all tags by frecency
235235- const result = await api.datastore.getTagsByFrecency();
236236- if (!result.success) {
237237- console.log('Failed to get tags');
238238- return { success: false };
239239- }
240240-241241- if (result.data.length === 0) {
242242- console.log('No tags yet');
243243- } else {
244244- console.log('All tags (by frecency):');
245245- result.data.slice(0, 20).forEach(t => {
246246- console.log(' -', t.name, `(used ${t.frequency}x, frecency: ${t.frecencyScore?.toFixed(1) || 0})`);
247247- });
248248- }
249249- return { success: true, tags: result.data };
250250- }
251251-252252- const item = await findItemByUrl(activeWindow.url);
253253- if (!item) {
254254- console.log('No tags for:', activeWindow.url);
255255- return { success: true, tags: [] };
256256- }
257257-258258- const tags = await getTagsForItem(item.id);
259259-260260- if (tags.length === 0) {
261261- console.log('No tags for:', activeWindow.url);
262262- } else {
263263- console.log('Tags for', activeWindow.url + ':');
264264- tags.forEach(t => console.log(' -', t.name));
265265- }
266266-267267- return { success: true, tags };
268268- }
269269- },
270270- {
271271- name: 'untag',
272272- description: 'Remove tags from the active window URL',
273273- async execute(ctx) {
274274- if (!ctx.search) {
275275- console.log('Usage: untag <tag1> [tag2] ...');
276276- return { success: false, error: 'No tags specified' };
277277- }
278278-279279- // Delegate to tag -r
280280- return commands[0].execute({ search: '-r ' + ctx.search });
281281- }
282282- }
283283-];
284284-285285-export default {
286286- commands
287287-};
-79
extensions/cmd/commands/tagset.js
···11-/**
22- * Tagset command - creates tagset items in the datastore
33- * Tagsets are items of type='tagset' that exist solely to hold a combination of tags
44- * Useful for creating quick reference collections or categorization markers
55- */
66-import api from 'peek://app/api.js';
77-88-/**
99- * Create a new tagset with the specified tags
1010- * @param {string} tagsString - Comma-separated list of tag names
1111- * @returns {Promise<string>} The ID of the created tagset
1212- */
1313-const createTagset = async (tagsString) => {
1414- // Parse tags from comma-separated string
1515- const tagNames = tagsString
1616- .split(',')
1717- .map(t => t.trim())
1818- .filter(t => t.length > 0);
1919-2020- if (tagNames.length === 0) {
2121- throw new Error('No valid tags provided');
2222- }
2323-2424- // Create the tagset item
2525- const result = await api.datastore.addItem('tagset', {
2626- content: tagNames.join(', ')
2727- });
2828-2929- if (!result.success) {
3030- throw new Error(result.error || 'Failed to create tagset');
3131- }
3232-3333- const itemId = result.data.id;
3434-3535- // Add each tag to the tagset
3636- for (const tagName of tagNames) {
3737- const tagResult = await api.datastore.getOrCreateTag(tagName);
3838- if (tagResult.success) {
3939- await api.datastore.tagItem(itemId, tagResult.data.tag.id);
4040- }
4141- }
4242-4343- // Also add the 'from:cmd' tag to track origin
4444- const fromCmdResult = await api.datastore.getOrCreateTag('from:cmd');
4545- if (fromCmdResult.success) {
4646- await api.datastore.tagItem(itemId, fromCmdResult.data.tag.id);
4747- }
4848-4949- return { id: itemId, tags: tagNames };
5050-};
5151-5252-// Commands
5353-const commands = [
5454- {
5555- name: 'tagset',
5656- description: 'Create a tagset with specified tags',
5757- async execute(ctx) {
5858- if (ctx.search) {
5959- try {
6060- const { id, tags } = await createTagset(ctx.search);
6161- console.log(`Tagset created with ID: ${id}`);
6262- console.log(`Tags: ${tags.join(', ')}`);
6363- api.publish('editor:changed', { action: 'add', itemId: id }, api.scopes.GLOBAL);
6464- return { success: true, message: `Tagset created with tags: ${tags.join(', ')}` };
6565- } catch (error) {
6666- console.error('Failed to create tagset:', error);
6767- return { success: false, message: error.message };
6868- }
6969- } else {
7070- api.publish('editor:add', { type: 'tagset' }, api.scopes.GLOBAL);
7171- return { success: true, message: 'Opening editor' };
7272- }
7373- }
7474- }
7575-];
7676-7777-export default {
7878- commands
7979-};